lista de comprensión vs. lambda + filtro

859

Me encontré con una necesidad básica de filtrado: tengo una lista y tengo que filtrarla por un atributo de los elementos.

Mi código se veía así:

my_list = [x for x in my_list if x.attribute == value]

Pero luego pensé, ¿no sería mejor escribirlo así?

my_list = filter(lambda x: x.attribute == value, my_list)

Es más legible, y si es necesario para el rendimiento, se podría sacar la lambda para obtener algo.

La pregunta es: ¿hay alguna advertencia al usar la segunda forma? ¿Alguna diferencia de rendimiento? ¿Me estoy perdiendo el Pythonic Way ™ por completo y debería hacerlo de otra manera (como usar itemgetter en lugar de lambda)?

Agos
fuente
19
Un mejor ejemplo sería un caso en el que ya tenía una función bien nombrada para usar como predicado. En ese caso, creo que mucha más gente estaría de acuerdo en que filterera más legible. Cuando tiene una expresión simple que se puede usar como está en una listacomp, pero tiene que estar envuelta en una lambda (o construida de manera similar a partir de partialo operatorfunciones, etc.) para pasar filter, entonces es cuando ganan las listas.
abarnert
3
Debería decirse que en Python3 al menos, el retorno de filteres un objeto generador de filtro, no una lista.
Matteo Ferla

Respuestas:

589

Es extraño cuánto varía la belleza para diferentes personas. Encuentro la comprensión de la lista mucho más clara que filter+ lambda, pero use la que le resulte más fácil.

Hay dos cosas que pueden retrasar su uso filter.

El primero es la sobrecarga de la llamada de función: tan pronto como use una función de Python (ya sea creada por defo lambda) es probable que el filtro sea más lento que la comprensión de la lista. Es casi seguro que no sea suficiente, y no debe pensar mucho en el rendimiento hasta que haya cronometrado su código y haya encontrado que es un cuello de botella, pero la diferencia estará ahí.

La otra sobrecarga que podría aplicarse es que la lambda se ve obligada a acceder a una variable de ámbito ( value). Eso es más lento que acceder a una variable local y en Python 2.x la comprensión de la lista solo accede a las variables locales. Si está utilizando Python 3.x, la comprensión de la lista se ejecuta en una función separada, por lo que también se accederá a valuetravés de un cierre y esta diferencia no se aplicará.

La otra opción a considerar es usar un generador en lugar de una lista de comprensión:

def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

Luego, en su código principal (que es donde la legibilidad realmente importa) ha reemplazado tanto la comprensión de la lista como el filtro con un nombre de función con significado significativo.

Duncan
fuente
68
+1 para el generador. Tengo un enlace en casa a una presentación que muestra cuán increíbles pueden ser los generadores. También puede reemplazar la comprensión de la lista con una expresión generadora simplemente cambiando []a (). Además, estoy de acuerdo en que el borrador de la lista es más hermoso.
Wayne Werner
1
En realidad, sin filtro es más rápido. Simplemente ejecute un par de puntos de referencia rápidos utilizando algo como stackoverflow.com/questions/5998245/…
skqr
2
@skqr es mejor usar timeit para los puntos de referencia, pero dé un ejemplo en el que encuentre filterque es más rápido usando una función de devolución de llamada de Python.
Duncan
8
@ tnq177 Es la presentación de David Beasley sobre generadores - dabeaz.com/generators
Wayne Werner
2
@ VictorSchröder sí, tal vez no estaba claro. Lo que intentaba decir era que en el código principal necesitas poder ver la imagen más grande. En la pequeña función auxiliar solo necesita preocuparse por esa función, lo que está sucediendo afuera puede ignorarse.
Duncan
237

Este es un tema algo religioso en Python. A pesar de que Guido consideró eliminarlo map, filtery reducede Python 3 , hubo suficiente reacción violenta que al final solo reducese movió de las funciones integradas a functools.reduce .

Personalmente, encuentro las comprensiones de listas más fáciles de leer. Es más explícito lo que está sucediendo en la expresión, [i for i in list if i.attribute == value]ya que todo el comportamiento está en la superficie, no dentro de la función de filtro.

No me preocuparía demasiado la diferencia de rendimiento entre los dos enfoques, ya que es marginal. Realmente solo optimizaría esto si resultara ser el cuello de botella en su aplicación, lo cual es poco probable.

Además, dado que el BDFL quería filterpasar del lenguaje, seguramente eso automáticamente hace que las comprensiones de listas sean más pitónicas ;-)

Tendayi Mawushe
fuente
1
Gracias por los enlaces a la entrada de Guido, si nada más para mí significa que trataré de no usarlos más, para no tener el hábito y no
apoyaré
1
¡pero reducir es lo más complejo que se puede hacer con herramientas simples! ¡El mapa y el filtro son triviales para reemplazar con comprensiones!
njzk2
8
no sabía reducir fue degradado en Python3. gracias por la perspicacia! reduce () sigue siendo bastante útil en informática distribuida, como PySpark. Creo que fue un error ..
Tagar
1
@Tagar todavía puede usar reducir, solo tiene que importarlo desde functools
icc97
69

Dado que cualquier diferencia de velocidad está destinada a ser minúscula, usar filtros o comprender las listas se reduce a una cuestión de gustos. En general, me inclino a usar las comprensiones (que parece estar de acuerdo con la mayoría de las otras respuestas aquí), pero hay un caso en el que prefiero filter.

Un caso de uso muy frecuente es extraer los valores de algún X iterable sujeto a un predicado P (x):

[x for x in X if P(x)]

pero a veces quieres aplicar alguna función a los valores primero:

[f(x) for x in X if P(f(x))]


Como ejemplo específico, considere

primes_cubed = [x*x*x for x in range(1000) if prime(x)]

Creo que esto se ve un poco mejor que usar filter. Pero ahora considera

prime_cubes = [x*x*x for x in range(1000) if prime(x*x*x)]

En este caso queremos filtercontra el valor post-calculado. Además del problema de calcular el cubo dos veces (imagine un cálculo más costoso), está el problema de escribir la expresión dos veces, violando la estética DRY . En este caso, sería apto para usar

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
IJ Kennedy
fuente
77
¿No consideraría usar el primo a través de otra lista de comprensión? Tales como[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9
20
x*x*xno puede ser un número primo, como lo ha sido x^2y xcomo factor, el ejemplo realmente no tiene sentido de manera matemática, pero tal vez aún sea útil. (Tal vez podríamos encontrar algo mejor, aunque?)
Zelphir Kaltstahl
3
Tenga en cuenta que podemos usar una expresión generadora para el último ejemplo si no queremos consumir memoria:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq
44
@MateenUlhaq esto se puede optimizar para prime_cubes = [1]ahorrar tanto memoria como ciclos de CPU ;-)
Dennis Krupenik
77
@DennisKrupenik O mejor dicho,[]
Mateen Ulhaq
29

Aunque filterpuede ser la "forma más rápida", la "forma pitónica" sería no preocuparse por esas cosas a menos que el rendimiento sea absolutamente crítico (¡en cuyo caso no estaría usando Python!).

Umang
fuente
10
Comentario tardío a un argumento que se ve a menudo: a veces es importante tener un análisis ejecutado en 5 horas en lugar de 10, y si eso se puede lograr tomando una hora optimizando el código de Python, puede valer la pena (especialmente si uno es cómodo con python y no con lenguajes más rápidos).
bli
¡Pero lo más importante es cuánto nos demora el código fuente al intentar leerlo y comprenderlo!
thoni56
20

Pensé que solo agregaría que en Python 3, filter () es en realidad un objeto iterador, por lo que tendría que pasar su llamada al método de filtro a list () para construir la lista filtrada. Entonces en Python 2:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = filter(lambda num: num % 2 == 0, lst_a)

las listas byc tienen los mismos valores, y se completaron aproximadamente al mismo tiempo que filter () era equivalente [x para x en y si z]. Sin embargo, en 3, este mismo código dejaría la lista c que contiene un objeto de filtro, no una lista filtrada. Para producir los mismos valores en 3:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = list(filter(lambda num: num %2 == 0, lst_a))

El problema es que list () toma un iterable como argumento y crea una nueva lista a partir de ese argumento. El resultado es que usar el filtro de esta manera en Python 3 toma hasta el doble de tiempo que el método [x para x en y si z] porque debe iterar sobre la salida del filtro (), así como la lista original.

Jim50
fuente
13

Una diferencia importante es que la comprensión de la lista devolverá un listtiempo, mientras que el filtro devuelve un filter, que no puede manipular como un list(es decir, invocarlo len, que no funciona con el retorno de filter).

Mi propio autoaprendizaje me llevó a un problema similar.

Dicho esto, si hay una manera de obtener el resultado listde un filter, un poco como lo haría en .NET cuando lo haga lst.Where(i => i.something()).ToList(), tengo curiosidad por saberlo.

EDITAR: Este es el caso de Python 3, no 2 (ver discusión en los comentarios).

Adeynack
fuente
44
filter devuelve una lista y podemos usar len en ella. Al menos en mi Python 2.7.6.
thiruvenkadam
77
No es el caso en Python 3. a = [1, 2, 3, 4, 5, 6, 7, 8] f = filter(lambda x: x % 2 == 0, a) lc = [i for i in a if i % 2 == 0] >>> type(f) <class 'filter'> >>> type(lc) <class 'list'>
Adeynack
3
"si hay una manera de tener la lista resultante ... Tengo curiosidad por saberlo". Sólo tiene que llamar list()en el resultado: list(filter(my_func, my_iterable)). Y, por supuesto, podría reemplazar listcon set, o tuple, o cualquier otra cosa que requiera un iterable. Pero para cualquiera que no sean programadores funcionales, el caso es aún más fuerte para usar una comprensión de la lista en lugar de filteruna conversión más explícita list.
Steve Jessop
10

Encuentro la segunda forma más legible. Te dice exactamente cuál es la intención: filtrar la lista.
PD: no use 'lista' como nombre de variable

incrédulo
fuente
7

generalmente filteres un poco más rápido si se usa una función integrada.

Esperaría que la comprensión de la lista sea un poco más rápida en su caso

John La Rooy
fuente
python -m timeit 'filter (lambda x: x en [1,2,3,4,5], rango (10000000))' 10 bucles, lo mejor de 3: 1,44 segundos por bucle python -m timeit '[x para x en el rango (10000000) si x en [1,2,3,4,5]] '10 bucles, lo mejor de 3: 860 ms por bucle ¡¿Realmente no ?!
giaosudau
@sepdau, las funciones lambda no son incorporadas. La comprensión de las listas ha mejorado en los últimos 4 años; ahora la diferencia es insignificante de todos modos, incluso con funciones integradas
John La Rooy el
7

El filtro es solo eso. Filtra los elementos de una lista. Puede ver que la definición menciona lo mismo (en el enlace de documentos oficiales que mencioné antes). Mientras que la comprensión de la lista es algo que produce una nueva lista después de actuar sobre algo de la lista anterior. , digamos, un tipo de datos completamente nuevo. Como convertir enteros a cadenas, etc.)

En su ejemplo, es mejor usar filtro que comprensión de listas, según la definición. Sin embargo, si lo desea, diga other_attribute de los elementos de la lista, en su ejemplo se debe recuperar como una nueva lista, entonces puede usar la comprensión de la lista.

return [item.other_attribute for item in my_list if item.attribute==value]

Así es como realmente recuerdo acerca de la comprensión de filtros y listas. Elimine algunas cosas dentro de una lista y mantenga los otros elementos intactos, use filtro. Use un poco de lógica por su cuenta en los elementos y cree una lista diluida adecuada para algún propósito, use la comprensión de la lista.

thiruvenkadam
fuente
2
Me alegrará saber el motivo de la votación negativa para no repetirlo de nuevo en ningún momento en el futuro.
thiruvenkadam
la definición de filtro y comprensión de listas no era necesaria, ya que su significado no se debatía. Se presenta una comprensión de la lista solo para listas "nuevas", pero no se argumenta.
Agos
Usé la definición para decir que filter te da una lista con los mismos elementos que son verdaderos para un caso, pero con la comprensión de la lista podemos modificar los elementos en sí mismos, como convertir int en str. Pero punto tomado :-)
thiruvenkadam
4

Aquí hay una pieza corta que uso cuando necesito filtrar algo después de la comprensión de la lista. Solo una combinación de filtro, lambda y listas (también conocida como la lealtad de un gato y la limpieza de un perro).

En este caso, estoy leyendo un archivo, eliminando líneas en blanco, líneas comentadas y cualquier cosa después de un comentario en una línea:

# Throw out blank lines and comments
with open('file.txt', 'r') as lines:        
    # From the inside out:
    #    [s.partition('#')[0].strip() for s in lines]... Throws out comments
    #   filter(lambda x: x!= '', [s.part... Filters out blank lines
    #  y for y in filter... Converts filter object to list
    file_contents = [y for y in filter(lambda x: x != '', [s.partition('#')[0].strip() for s in lines])]
rharder
fuente
Esto logra mucho en muy poco código. Sin embargo, creo que podría ser demasiado lógico en una línea entender fácilmente y la legibilidad es lo que cuenta.
Zelphir Kaltstahl
Podrías escribir esto comofile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop el
4

Además de la respuesta aceptada, hay un caso de esquina en el que debe usar filtro en lugar de una comprensión de lista. Si la lista no se puede compartir, no puede procesarla directamente con una comprensión de la lista. Un ejemplo del mundo real es si usas pyodbcpara leer resultados de una base de datos. Los fetchAll()resultados de cursores una lista inquebrantable. En esta situación, para manipular directamente los resultados devueltos, se debe utilizar el filtro:

cursor.execute("SELECT * FROM TABLE1;")
data_from_db = cursor.fetchall()
processed_data = filter(lambda s: 'abc' in s.field1 or s.StartTime >= start_date_time, data_from_db) 

Si utiliza la comprensión de la lista aquí, obtendrá el error:

TypeError: tipo no compartible: 'lista'

CWpraen
fuente
1
todas las listas son insalvables en >>> hash(list()) # TypeError: unhashable type: 'list'segundo lugar, esto funciona bien:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger
"Si la lista no se puede compartir, no se puede procesar directamente con una comprensión de la lista". Esto no es cierto, y todas las listas son inquebrantables de todos modos.
juanpa.arrivillaga
3

Me tomó un tiempo familiarizarme con el higher order functions filtery map. Así que me acostumbré a ellos y en realidad me gustó, filterya que era explícito que se filtra manteniendo lo que es verdadero y me sentí bien al saber algunos functional programmingtérminos.

Luego leí este pasaje (Fluent Python Book):

Las funciones de mapa y filtro todavía están integradas en Python 3, pero desde la introducción de las comprensiones de listas y las expresiones de generador, no son tan importantes. Un listcomp o un genexp hace el trabajo de mapa y filtro combinados, pero es más legible.

Y ahora pienso, ¿por qué molestarse con el concepto de filter/ mapsi puede lograrlo con expresiones idiomáticas ya ampliamente difundidas como las comprensiones de listas? Además mapsy filtersson un tipo de funciones. En este caso prefiero usar Anonymous functionslambdas.

Finalmente, solo por el hecho de haberlo probado, he cronometrado ambos métodos ( mapy listComp) y no vi ninguna diferencia de velocidad relevante que justificara hacer argumentos al respecto.

from timeit import Timer

timeMap = Timer(lambda: list(map(lambda x: x*x, range(10**7))))
print(timeMap.timeit(number=100))

timeListComp = Timer(lambda:[(lambda x: x*x) for x in range(10**7)])
print(timeListComp.timeit(number=100))

#Map:                 166.95695265199174
#List Comprehension   177.97208347299602
usuario1767754
fuente
0

Curiosamente en Python 3, veo que el filtro funciona más rápido que las listas de comprensión.

Siempre pensé que las comprensiones de la lista serían más efectivas. Algo así como: [nombre para el nombre en brand_names_db si el nombre no es Ninguno] El código de bytes generado es un poco mejor.

>>> def f1(seq):
...     return list(filter(None, seq))
>>> def f2(seq):
...     return [i for i in seq if i is not None]
>>> disassemble(f1.__code__)
2         0 LOAD_GLOBAL              0 (list)
          2 LOAD_GLOBAL              1 (filter)
          4 LOAD_CONST               0 (None)
          6 LOAD_FAST                0 (seq)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION            1
         12 RETURN_VALUE
>>> disassemble(f2.__code__)
2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10cfcaa50, file "<stdin>", line 2>)
          2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_FAST                0 (seq)
          8 GET_ITER
         10 CALL_FUNCTION            1
         12 RETURN_VALUE

Pero en realidad son más lentos:

   >>> timeit(stmt="f1(range(1000))", setup="from __main__ import f1,f2")
   21.177661532000116
   >>> timeit(stmt="f2(range(1000))", setup="from __main__ import f1,f2")
   42.233950221000214
Rod Senra
fuente
8
La comparación no es válida . Primero, no está pasando una función lambda a la versión de filtro, lo que la convierte en la función de identidad predeterminada. Al definir if not Noneen la comprensión de la lista, está definiendo una función lambda (observe la MAKE_FUNCTIONdeclaración). Segundo, los resultados son diferentes, ya que la versión de comprensión de la lista eliminará solo el Nonevalor, mientras que la versión del filtro eliminará todos los valores "falsos". Dicho esto, todo el propósito de la microbenchmarking es inútil. ¡Esas son un millón de iteraciones, multiplicadas por mil artículos! La diferencia es insignificante .
Victor Schröder
-7

Mi toma

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
tim
fuente
3
inunca se dijo que fuera un dict, y no hay necesidad de hacerlo limit. Aparte de eso, ¿en qué se diferencia esto de lo que sugirió el OP y cómo responde a la pregunta?