lista de comprensión vs lambda + filtro

Resulta que tengo 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 fuera necesario para el rendimiento, la lambda podría sacarse para obtener algo.

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

Es extraño cómo varía la belleza para diferentes personas. La comprensión de la lista me parece mucho más clara que filter + lambda , pero uso lo que encuentre más fácil. Sin embargo, deje de dar nombres a sus variables que ya se usaron para las funciones integradas, eso es confuso. [ La pregunta originalmente utilizó la list como nombre de variable pero se actualizó en respuesta a esta respuesta. ]

Hay dos cosas que pueden ralentizar el uso del filter .

La primera es la sobrecarga de llamada a la función: tan pronto como use una función Python (ya sea creada por def o lambda ), es probable que el filtro sea más lento que la lista de comprensión. Es casi seguro que no es suficiente para importar, y no deberías pensar mucho en el rendimiento hasta que hayas cronometrado tu código y hayas encontrado 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 ( value ) con ámbito. Eso es más lento que acceder a una variable local y en Python 2.x la comprensión de la lista solo tiene acceso 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 tendrá acceso al value mediante 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 realmente importa la legibilidad) ha reemplazado la comprensión y el filtro de la lista con un nombre de función con la esperanza de que sea significativo.

Este es un tema un tanto religioso en Python. A pesar de que Guido consideró eliminar el map , el filter y la reduce de Python 3 , hubo una reacción suficiente que al final solo se reduce de incorporados a functools.reduce .

Personalmente me parece más fácil leer las listas de comprensión. Es más explícito lo que sucede a partir de la expresión [i for i in list if i.attribute == value] ya que todo el comportamiento está en la superficie y 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 que el filter desapareciera del idioma, seguramente eso hace automáticamente que las comprensiones de listas sean más Pythonic 😉

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

Un caso de uso muy frecuente es extraer los valores de algunos X iterables sujetos 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 el filter . Pero ahora considera

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

En este caso queremos filter contra el valor post-computado. Además del problema de calcular el cubo dos veces (imagine un cálculo más costoso), existe el problema de escribir la expresión dos veces, violando la estética DRY . En este caso estaría dispuesto a usar

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

Aunque el filter puede ser la “forma más rápida”, la “manera Pythonic” sería no preocuparse por tales cosas a menos que el rendimiento sea absolutamente crítico (¡en cuyo caso no usarías Python!).

Pensé que solo agregaría que en python 3, filter () es en realidad un objeto iterador, así que tendrías que pasar la llamada del método de filtro a list () para construir la lista filtrada. Así que 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 b y c tienen los mismos valores, y se completaron casi 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 if z] porque tiene que iterar sobre la salida del filtro () así como la lista original.

Una diferencia importante es que la comprensión de la lista devolverá una list mientras que el filtro devuelve un filter , que no puede manipular como una list (es decir, len llamada en él, que no funciona con la devolución del filter ).

Mi propio autoaprendizaje me llevó a un problema similar.

Dicho esto, si hay una manera de obtener la list resultante de un filter , un poco como lo haría en .NET cuando hace lst.Where(i => i.something()).ToList() , estoy Curioso por saberlo.

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

Encuentro la segunda forma más legible. Le dice exactamente cuál es la intención: filtre la lista.
PS: no use ‘lista’ como nombre de variable

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é anteriormente). Considerando que, la comprensión de la lista es algo que produce una nueva lista después de actuar sobre algo en la lista anterior. (La comprensión de la lista y del filtro crea una nueva lista y no realiza una operación en lugar de la lista anterior. Una nueva lista aquí es algo así como una lista con , digamos, un tipo de datos completamente nuevo. Como convertir números enteros a cadenas, etc.)

En su ejemplo, es mejor usar filtro que comprensión de lista, según la definición. Sin embargo, si lo desea, diga other_attribute de los elementos de la lista, en su ejemplo se recuperará como una nueva lista, luego 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 listas y filtros. Elimine algunas cosas dentro de una lista y mantenga los otros elementos intactos, use el 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.

Generalmente, el filter es un poco más rápido si se usa una función incorporada.

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

Aquí hay una breve pieza 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, borrando 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])] 

Además de la respuesta aceptada, hay un caso de esquina en el que debe usar el filtro en lugar de una lista de comprensión. Si la lista es inestable, no puede procesarla directamente con una lista de comprensión. Un ejemplo del mundo real es si utiliza pyodbc para leer resultados de una base de datos. Los resultados de fetchAll() del cursor son una lista inasible. En esta situación, para manipular directamente los resultados devueltos, se debe usar 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 lista de comprensión aquí obtendrá el error:

TypeError: tipo descargable: ‘lista’

Me tomó un tiempo familiarizarme con el filter y map higher order functions . Así que me acostumbré a ellos y realmente me gustó el filter ya que era explícito que filtraba manteniendo lo que fuera verdad y me sentía bien porque sabía algunos términos de functional programming .

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 generadoras, no son tan importantes. Un listcomp o un genexp hace el trabajo de mapeo y filtro combinados, pero es más legible.

Y ahora pienso, ¿por qué molestarse con el concepto de filter / map si puede lograrlo con idiomas ya ampliamente difundidos, como las listas de comprensión? Además los maps y filters son una especie de funciones. En este caso prefiero usar las Anonymous functions lambdas.

Finalmente, solo por el hecho de haberlo probado, he cronometrado ambos métodos ( map y listComp ) y no vi ninguna diferencia de velocidad relevante que justifique 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 

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

Siempre pensé que las listas de comprensión serían más eficaces. Algo como: [nombre para el nombre en brand_names_db si el nombre no es Ninguno] El bytecode 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 ( at 0x10cfcaa50, file "", line 2>) 2 LOAD_CONST 2 ('f2..') 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 

Mi toma

 def filter_list(list, key, value, limit=None): return [i for i in list if i[key] == value][:limit]