¿Cómo puedo particionar (dividir, dividir) una lista en función de una condición?

9 minutos de lectura

Avatar de usuario de Parand
Parand

Tengo un código como:

good = [x for x in mylist if x in goodvals]
bad = [x for x in mylist if x not in goodvals]

El objetivo es dividir el contenido de mylist en otras dos listas, en función de si cumplen o no una condición.

¿Cómo puedo hacer esto con más elegancia? ¿Puedo evitar hacer dos iteraciones separadas sobre mylist? ¿Puedo mejorar el rendimiento al hacerlo?

  • Aterricé aquí buscando una manera de tener una condición en la declaración del generador de conjuntos, su pregunta respondió a mi pregunta 🙂

    – Anuvrat Parashar

    21 de junio de 2012 a las 13:27

  • dividir es una descripción desafortunada de esta operación, ya que ya tiene un significado específico con respecto a las cadenas de Python. Creo dividir es una palabra más precisa (o al menos menos sobrecargada en el contexto de iterables de Python) para describir esta operación. Aterricé aquí buscando una lista equivalente a str.split()a dividir la lista en una colección ordenada de sublistas consecutivas. P.ej split([1,2,3,4,5,3,6], 3) -> ([1,2],[4,5],[6])Opuesto a divisor elementos de una lista por categoría.

    – Guiso

    17 dic 2015 a las 16:24


  • Discusión del mismo tema en python-list.

    – Xiong Chiamiov

    10/10/2016 a las 19:13

  • IMAGE_TYPES debe ser un conjunto en lugar de una tupla: IMAGE_TYPES = set('.jpg','.jpeg','.gif','.bmp','.png'). n(1) en lugar de n(o/2), prácticamente sin diferencia en legibilidad.

    – ChaimG

    5 de marzo de 2017 a las 17:58


avatar de usuario de dbr
dbr

good = [x for x in mylist if x in goodvals]
bad  = [x for x in mylist if x not in goodvals]

¿Cómo puedo hacer esto con más elegancia?

Ese código ya es perfectamente elegante.

Puede haber ligeras mejoras de rendimiento al usar sets, pero la diferencia es trivial. set los enfoques basados ​​también descartarán los duplicados y no preservarán el orden de los elementos. También encuentro que la lista de comprensión es mucho más fácil de leer.

De hecho, podríamos incluso más simplemente usar un for bucle:

good, bad = [], []

for x in mylist:
    if x in goodvals:
        good.append(f)
    else:
        bad.append(f)

Este enfoque facilita la adición de lógica adicional. Por ejemplo, el código se modifica fácilmente para descartar None valores:

good, bad = [], []

for x in mylist:
    if x is None:
        continue
    if x in goodvals:
        good.append(f)
    else:
        bad.append(f)

  • ¿No hay una forma de comprensión de la lista sin tener que recorrer la lista dos veces?

    – balki

    21 de julio de 2012 a las 15:42

  • El problema es que esto viola el principio DRY. Sería bueno si hubiera una mejor manera de hacer esto.

    – Antimonio

    9 mayo 2013 a las 18:03

  • Una vez que aumenta el apetito por la programación funcional (Haskell) o el estilo funcional (LINQ), comenzamos a oler Python para su edad: [x for x in blah if ...] – detallado, lambda es torpe y limitado… Se siente como conducir el mejor auto de 1995 en la actualidad. No es lo mismo que entonces.

    – Tomasz Gandor

    24 mayo 2015 a las 13:52

  • @TomaszGandor FTR, Haskell es más viejo que Python (y de hecho influyó en su diseño). Creo que la sintaxis para la comprensión de listas y las lambdas se mantuvo deliberadamente un poco detallada, tal vez para desalentar su uso excesivo. Lo cual es un poco arriesgado… por mucho que me guste Haskell, puedo ver por qué mucha gente encuentra Python generalmente más legible.

    – a la izquierda

    30/09/2015 a las 20:04

  • el bucle for simple es la mejor manera de hacer esto… un solo bucle, muy claro y legible

    – Anentrópico

    21 de abril de 2016 a las 10:04

  • Probablemente tengas razón en que esto viola el principio YAGNI. Se basa en la suposición de que la cantidad de listas diferentes en las que se pueden dividir las cosas crecerá en el futuro.

    – Hormigas Aasma

    4 de junio de 2009 a las 15:33

  • Puede ser mucho código, pero si [ x for x in my_list if ExpensiveOperation(x) ] lleva mucho tiempo ejecutarse, ¡ciertamente no querrás hacerlo dos veces!

    – dash-tom-bang

    10 de enero de 2013 a las 22:29

  • +1 por ofrecer múltiples variaciones, incluidas las basadas en iteradores y una solución específica “en X”. El OP “in goodvals” puede ser pequeño, pero reemplazarlo con un diccionario muy grande o un predicado costoso podría ser costoso. También reduce la necesidad de escribir la lista de comprensión dos veces. en todos lados es necesario, lo que reduce la probabilidad de introducir errores tipográficos o de usuario. Buena solución. ¡Gracias!

    – cod3monk3y

    16 de noviembre de 2013 a las 21:08

  • Tenga en cuenta que tee almacena todos los valores entre los iteradores que devuelve, por lo que realmente no ahorrará memoria si recorre un generador completo y luego el otro.

    – John LaRooy

    8 de agosto de 2014 a las 4:51

avatar de usuario de winden
viento

El problema con todas las soluciones propuestas es que escaneará y aplicará la función de filtrado dos veces. Haría una pequeña función simple como esta:

def split_into_two_lists(lst, f):
    a = []
    b = []
    for elem in lst:
        if f(elem):
            a.append(elem)
        else:
            b.append(elem)
    return a, b

De esa manera, no está procesando nada dos veces y tampoco está repitiendo el código.

  • Estoy de acuerdo. Estaba buscando una forma “elegante” (es decir, aquí significa corta e integrada/implícita) de hacer esto sin escanear la lista dos veces, pero parece (sin perfiles) ser el camino a seguir. Por supuesto, solo importaría de todos modos para grandes cantidades de datos.

    – Mateo Flaschen

    4 de junio de 2009 a las 8:32

  • En mi humilde opinión, si conoce una manera de hacerlo con menos uso de la CPU (y, por lo tanto, menos consumo de energía), no hay razón para no usarlo.

    – viento

    4 de junio de 2009 a las 19:46

  • @winden … Transferir todo mi Python a C. 😉

    – Elliot Cameron

    27/04/2016 a las 21:52

Avatar de usuario de Tom
Tomás

Mi opinión al respecto. Propongo un perezoso, de un solo paso, partition función, que conserva el orden relativo en las subsecuencias de salida.

1. Requisitos

Supongo que los requisitos son:

  • mantener el orden relativo de los elementos (por lo tanto, sin conjuntos ni diccionarios)
  • evaluar la condición solo una vez para cada elemento (por lo tanto, no usar (i)filter o groupby)
  • permitir el consumo perezoso de cualquiera de las secuencias (si podemos permitirnos calcularlas previamente, es probable que la implementación ingenua también sea aceptable)

2. split biblioteca

Mi partition función (presentada a continuación) y otras funciones similares se han convertido en una pequeña biblioteca:

Es instalable normalmente a través de PyPI:

pip install --user split

Para dividir una lista basada en condiciones, use partition función:

>>> from split import partition
>>> files = [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi') ]
>>> image_types = ('.jpg','.jpeg','.gif','.bmp','.png')
>>> images, other = partition(lambda f: f[-1] in image_types, files)
>>> list(images)
[('file1.jpg', 33L, '.jpg')]
>>> list(other)
[('file2.avi', 999L, '.avi')]

3. partition función explicada

Internamente, necesitamos construir dos subsecuencias a la vez, por lo que consumir solo una secuencia de salida obligará a calcular la otra también. Y necesitamos mantener el estado entre las solicitudes de los usuarios (almacenar elementos procesados ​​pero aún no solicitados). Para mantener el estado, uso dos colas de dos extremos (deques):

from collections import deque

SplitSeq la clase se encarga de la limpieza:

class SplitSeq:
    def __init__(self, condition, sequence):
        self.cond = condition
        self.goods = deque([])
        self.bads = deque([])
        self.seq = iter(sequence)

La magia sucede en su .getNext() método. es casi como .next()
de los iteradores, pero permite especificar qué tipo de elemento queremos esta vez. Detrás de escena, no descarta los elementos rechazados, sino que los coloca en una de las dos colas:

    def getNext(self, getGood=True):
        if getGood:
            these, those, cond = self.goods, self.bads, self.cond
        else:
            these, those, cond = self.bads, self.goods, lambda x: not self.cond(x)
        if these:
            return these.popleft()
        else:
            while 1: # exit on StopIteration
                n = self.seq.next()
                if cond(n):
                    return n
                else:
                    those.append(n)

Se supone que el usuario final debe usar partition función. Se necesita una función de condición y una secuencia (al igual que map o filter), y devuelve dos generadores. El primer generador construye una subsecuencia de elementos para los que se cumple la condición, el segundo construye la subsecuencia complementaria. Los iteradores y generadores permiten la división diferida incluso de secuencias largas o infinitas.

def partition(condition, sequence):
    cond = condition if condition else bool  # evaluate as bool if condition == None
    ss = SplitSeq(cond, sequence)
    def goods():
        while 1:
            yield ss.getNext(getGood=True)
    def bads():
        while 1:
            yield ss.getNext(getGood=False)
    return goods(), bads()

Elegí la función de prueba como el primer argumento para facilitar la aplicación parcial en el futuro (similar a cómo map y filter
tener la función de prueba como primer argumento).

  • Estoy de acuerdo. Estaba buscando una forma “elegante” (es decir, aquí significa corta e integrada/implícita) de hacer esto sin escanear la lista dos veces, pero parece (sin perfiles) ser el camino a seguir. Por supuesto, solo importaría de todos modos para grandes cantidades de datos.

    – Mateo Flaschen

    4 de junio de 2009 a las 8:32

  • En mi humilde opinión, si conoce una manera de hacerlo con menos uso de la CPU (y, por lo tanto, menos consumo de energía), no hay razón para no usarlo.

    – viento

    4 de junio de 2009 a las 19:46

  • @winden … Transferir todo mi Python a C. 😉

    – Elliot Cameron

    27/04/2016 a las 21:52

Avatar de usuario de Alan Isaac
alan isaac

Básicamente, me gusta el enfoque de Anders, ya que es muy general. Aquí hay una versión que pone el categorizador primero (para que coincida con la sintaxis del filtro) y usa un dictado predeterminado (se supone que es importado).

def categorize(func, seq):
    """Return mapping from categories to lists
    of categorized items.
    """
    d = defaultdict(list)
    for item in seq:
        d[func(item)].append(item)
    return d

  • Iba a tratar de seleccionar las declaraciones de zen de pitón que se aplican aquí, pero son demasiados para un comentario. =) Impresionante pieza de código.

    – jpmc26

    24/10/2014 a las 18:49

¿Ha sido útil esta solución?