¿Cómo puedo particionar (dividir, dividir) una lista según una condición?

Resuelto Parand asked hace 15 años • 40 respuestas

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 mylisten otras dos listas, en función de si cumplen o no una condición.

¿Cómo puedo hacer esto de manera más elegante? ¿ Puedo evitar hacer dos iteraciones separadas mylist? ¿Puedo mejorar el rendimiento al hacerlo?

Parand avatar Jun 04 '09 14:06 Parand
Aceptado

Itere manualmente, utilizando la condición para seleccionar una lista a la que se agregará cada elemento:

good, bad = [], []
for x in mylist:
    (bad, good)[x in goodvals].append(x)
John La Rooy avatar Aug 27 '2012 00:08 John La Rooy
good = [x for x in mylist if x in goodvals]
bad  = [x for x in mylist if x not in goodvals]

¿Existe una forma más elegante de hacer esto?

¡Ese código es perfectamente legible y extremadamente claro!

# files looks like: [ ('file1.jpg', 33L, '.jpg'), ('file2.avi', 999L, '.avi'), ... ]
IMAGE_TYPES = ('.jpg','.jpeg','.gif','.bmp','.png')
images = [f for f in files if f[2].lower() in IMAGE_TYPES]
anims  = [f for f in files if f[2].lower() not in IMAGE_TYPES]

De nuevo, ¡esto está bien!

Puede haber ligeras mejoras de rendimiento al usar conjuntos, pero es una diferencia trivial, y encuentro que la comprensión de la lista es mucho más fácil de leer, y no tienes que preocuparte de que el orden se estropee, que se eliminen duplicados, etc.

De hecho, puedo dar un paso más "hacia atrás" y simplemente usar un bucle for simple:

images, anims = [], []

for f in files:
    if f.lower() in IMAGE_TYPES:
        images.append(f)
    else:
        anims.append(f)

La comprensión o el uso de una lista set()está bien hasta que necesite agregar alguna otra verificación u otro poco de lógica; digamos que desea eliminar todos los archivos JPEG de 0 bytes, simplemente agregue algo como...

if f[1] == 0:
    continue
dbr avatar Jun 04 '2009 13:06 dbr

Aquí está el enfoque del iterador perezoso:

from itertools import tee

def split_on_condition(seq, condition):
    l1, l2 = tee((condition(item), item) for item in seq)
    return (i for p, i in l1 if p), (i for p, i in l2 if not p)

Evalúa la condición una vez por elemento y devuelve dos generadores, primero generando valores de la secuencia donde la condición es verdadera y el otro donde es falsa.

Como es vago, puedes usarlo en cualquier iterador, incluso uno infinito:

from itertools import count, islice

def is_prime(n):
    return n > 1 and all(n % i for i in xrange(2, n))

primes, not_primes = split_on_condition(count(), is_prime)
print("First 10 primes", list(islice(primes, 10)))
print("First 10 non-primes", list(islice(not_primes, 10)))

Por lo general, aunque el enfoque de devolución de listas no diferidas es mejor:

def split_on_condition(seq, condition):
    a, b = [], []
    for item in seq:
        (a if condition(item) else b).append(item)
    return a, b

Editar: para su caso de uso más específico de dividir elementos en diferentes listas mediante alguna clave, aquí hay una función genérica que hace eso:

DROP_VALUE = lambda _:_
def split_by_key(seq, resultmapping, keyfunc, default=DROP_VALUE):
    """Split a sequence into lists based on a key function.

        seq - input sequence
        resultmapping - a dictionary that maps from target lists to keys that go to that list
        keyfunc - function to calculate the key of an input value
        default - the target where items that don't have a corresponding key go, by default they are dropped
    """
    result_lists = dict((key, []) for key in resultmapping)
    appenders = dict((key, result_lists[target].append) for target, keys in resultmapping.items() for key in keys)

    if default is not DROP_VALUE:
        result_lists.setdefault(default, [])
        default_action = result_lists[default].append
    else:
        default_action = DROP_VALUE

    for item in seq:
        appenders.get(keyfunc(item), default_action)(item)

    return result_lists

Uso:

def file_extension(f):
    return f[2].lower()

split_files = split_by_key(files, {'images': IMAGE_TYPES}, keyfunc=file_extension, default='anims')
print split_files['images']
print split_files['anims']
Ants Aasma avatar Jun 04 '2009 09:06 Ants Aasma

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 procesará nada dos veces y tampoco repetirá el código.

winden avatar Jun 04 '2009 08:06 winden