Comprensión de listas frente a filtro lambda +

957

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, la lambda podría sacarse para ganar 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)?

3
  • 27
    Un mejor ejemplo sería un caso en el que ya tuviera una función con un nombre agradable 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 tal cual en un listcomp, pero tiene que estar envuelto en un lambda (o construido de manera similar a partir de funciones partialo operator, etc.) para pasar filter, ahí es cuando los listcomps ganan. 31/07/2013 a las 19:15
  • 9
    Debería decirse que en Python3 al menos, el retorno de filteres un objeto generador de filtro, no una lista. 29 de agosto de 2019 a las 8:59
  • 2
    ¿Más legible? Supongo que es una cuestión de gusto personal, pero para mí, la solución de comprensión de listas se ve como en inglés simple: "para cada elemento en my_list, tómelo solo si su atributo es igual a valor" (!?). Supongo que incluso un no programador podría intentar comprender más o menos lo que está pasando. En la segunda solución ... bueno ... ¿cuál es esa extraña palabra "lamba", para empezar? Una vez más, probablemente sea una cuestión de gusto personal, pero yo optaría por la solución de comprensión de listas todo el tiempo, independientemente de las posibles pequeñas diferencias en el rendimiento que básicamente solo interesan a los investigadores. 29 abr a las 7:10
648

Es extraño ver cuánta belleza varía de una persona a otra. 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 ralentizar el uso de filter.

La primera es la sobrecarga de llamadas a la 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 es suficiente para importar, y no debería pensar mucho en el rendimiento hasta que haya cronometrado su código y descubra 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 lista de comprensión 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 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 suerte significativo.

9
  • 75
    +1 para el generador. Tengo un enlace en casa a una presentación que muestra lo asombrosos que 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 la lista comp es más hermosa. 10/06/10 a las 13:03
  • 3
    @skqr es mejor usar timeit para los puntos de referencia, pero por favor 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
    17/06/15 a las 10:32
  • 10
    @ tnq177 Es la presentación de David Beasley sobre generadores - dabeaz.com/generators 8 de junio de 2016 a las 13:03
  • 2
    "... que es donde la legibilidad realmente importa ..." . Lo siento, pero la legibilidad siempre es importante, incluso en los casos (raros) en los que, llorando, tienes que renunciar a ella. 17 de enero de 2019 a las 8:45
  • 2
    @ VictorSchröder sí, tal vez no estaba claro. Lo que estaba tratando de decir es que en el código principal necesitas poder ver la imagen más grande. En la función de pequeño ayudante, solo necesita preocuparse por esa función, lo que está sucediendo afuera se puede ignorar.
    Duncan
    17/01/19 a las 9:15
258

Este es un problema algo religioso en Python. A pesar de que Guido consideró eliminar map, filtery reducede Python 3 , hubo suficiente reacción que al final solo reducese movió de los elementos integrados a functools.reduce .

Personalmente, encuentro que las listas por comprensión son más fáciles de leer. Es más explícito lo que está sucediendo a partir de 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 por 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 filterdesaparecer del lenguaje, seguramente eso automáticamente hace que las listas de comprensión sean más Pythonic ;-)

4
  • 1
    Gracias por los enlaces a la entrada de Guido, si nada más para mí, significa que intentaré no usarlos más, para no adquirir el hábito y no apoyar esa religión :)
    dashesy
    12 de junio de 2013 a las 1:17
  • 2
    ¡pero reducir es lo más complejo de hacer con herramientas simples! ¡El mapa y el filtro son triviales para reemplazarlos con comprensiones!
    njzk2
    30 de mayo de 2014 a las 20:22
  • 10
    no sabía que reduce fue degradado en Python3. gracias por la información! reduce () sigue siendo bastante útil en computación distribuida, como PySpark. Creo que fue un error..
    Tagar
    28/06/15 a las 16:10
  • 1
    @Tagar todavía puedes usar reduce solo tienes que importarlo desde functools
    icc97
    11/10/2017 a las 11:58
82

Dado que cualquier diferencia de velocidad está destinada a ser minúscula, utilizar filtros o listas por comprensión se reduce a una cuestión de gustos. En general, me inclino a usar 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 desea 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 usarlo 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 (imagina un cálculo más caro), 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)])
10
  • 10
    ¿No consideraría usar el número primo a través de otra comprensión de lista? Tal como[prime(i) for i in [x**3 for x in range(1000)]] 12 de marzo de 2015 a las 2:22
  • 29
    x*x*xno puede ser un número primo, ya que lo ha hecho x^2y xcomo factor, el ejemplo realmente no tiene sentido de una manera matemática, pero tal vez aún sea útil. (¿Quizás podríamos encontrar algo mejor?) 16 de septiembre de 2015 a las 12:06
  • 4
    Tenga en cuenta que podemos usar una expresión generadora en su lugar para el último ejemplo si no queremos consumir memoria: prime_cubes = filter(prime, (x*x*x for x in range(1000))) 27 de agosto de 2016 a las 8:13
  • 5
    @MateenUlhaq esto se puede optimizar prime_cubes = [1]para ahorrar memoria y ciclos de CPU ;-) 12/03/18 a las 10:21
  • 8
    @DennisKrupenik O mejor dicho, [] 12/03/18 a las 15:11
34

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

2
  • 12
    Comentario tardío a un argumento visto con frecuencia: a veces hace una diferencia 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
    23 de enero de 2017 a las 16:44
  • ¡Pero lo más importante es cuánto nos ralentiza el código fuente al intentar leerlo y entenderlo!
    thoni56
    13/12/19 a las 7:07
27

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 conteniendo 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 el doble de tiempo que el método [x para x en y si z] porque tiene que iterar sobre la salida de filter () así como sobre la lista original.

0
15

Una diferencia importante es que la comprensión de la lista devolverá un listtiempo mientras el filtro devuelve un filter, que no se 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 hace lst.Where(i => i.something()).ToList(), tengo curiosidad por saberlo.

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

3
  • 4
    filter devuelve una lista y podemos usar len en ella. Al menos en mi Python 2.7.6. 29 de enero de 2015 a las 7:33
  • 7
    No es el caso de 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'> 29/01/15 a las 17:33
  • 4
    "si hay alguna manera de tener la lista resultante ... tengo curiosidad por conocerla". Sólo tiene que llamar list()en el resultado de: list(filter(my_func, my_iterable)). Y, por supuesto, podría reemplazarlo listcon set, o tuple, o cualquier otra cosa que requiera un iterable. Pero para cualquiera que no sea programador funcional, el caso es aún más fuerte para usar una lista de comprensión en lugar de filteruna conversión más explícita a list. 26/04/2016 a las 10:54
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

9

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). 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. (Tanto el filtro como la comprensión de la lista crean una nueva lista y no realizan la 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 en cadenas, etc.)

En su ejemplo, es mejor utilizar el filtro que la 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 va a 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 recuerdo sobre la comprensión de filtros y listas. Elimine algunas cosas dentro de una lista y mantenga intactos los otros elementos, 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 listas.

3
  • 2
    Me alegrará saber el motivo de la votación negativa para no repetirlo en ningún otro lugar en el futuro. 29 de enero de 2015 a las 7:41
  • 1
    la definición de filtro y la comprensión de listas no eran necesarias, ya que su significado no estaba siendo debatido. Se presenta, pero no se argumenta, que la comprensión de una lista debe usarse sólo para listas "nuevas".
    Agos
    02/02/15 a las 11:14
  • Usé la definición para decir que el filtro le 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 :-) 2 feb 2015 a las 14:02
8

generalmente filteres 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

2
  • 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 mseg por bucle ¡¿No realmente ?! 27/11/14 a las 16:27
  • @sepdau, las funciones lambda no están integradas. La comprensión de las listas ha mejorado en los últimos 4 años; ahora la diferencia es insignificante de todos modos, incluso con las funciones integradas. 27/11/2014 a las 21:08
6

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 conocido como la lealtad de un gato y la limpieza de un perro).

En este caso, estoy leyendo un archivo, quitando 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])]
2
  • De hecho, esto logra mucho en muy poco código. Creo que podría ser un poco demasiado lógico en una línea para entenderlo fácilmente y, sin embargo, lo que cuenta es la legibilidad. 16/09/15 a las 11:50
  • Podrías escribir esto como file_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines))) 26/04/2016 a las 10:59
4

Me tomó un tiempo familiarizarme con higher order functions filtery map. Así que me acostumbré a ellos y realmente 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):

The map and filter functions are still builtins in Python 3, but since the introduction of list comprehensions and generator ex‐ pressions, they are not as important. A listcomp or a genexp does the job of map and filter combined, but is more readable.

Y ahora pienso, ¿por qué molestarse con el concepto de filter/ mapsi puede lograrlo con modismos ya ampliamente difundidos como listas por comprensión? Además mapsy filtersson una especie de funciones. En este caso prefiero usar Anonymous functionslambdas.

Finalmente, solo por el simple hecho de probarlo, he cronometrado ambos métodos ( mapy listComp) y no vi ninguna diferencia de velocidad relevante que justificara argumentar 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
4

Además de la respuesta aceptada, hay un caso de esquina en el que debe usar un filtro en lugar de una lista de comprensión. Si la lista no se puede dividir, no puede procesarla directamente con una lista de comprensión. Un ejemplo del mundo real es si usa pyodbcpara leer resultados de una base de datos. Los fetchAll()resultados de cursores una lista indescifrable. 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 usa la comprensión de lista aquí, obtendrá el error:

TypeError: unhashable type: 'list'

2
  • 2
    todas las listas no se pueden clasificar 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] 29/01/20 a las 10:22
  • 1
    "Si la lista no se puede codificar, no se puede procesar directamente con una lista de comprensión". Esto no es cierto y, de todos modos , todas las listas no se pueden clasificar. 7 de mayo de 20 a las 5:30 p.m.
1

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

Siempre pensé que las listas por comprensión serían más eficaces. Algo como: [nombre por nombre en brand_names_db si el nombre no es None] 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
2
  • 10
    Comparación no válida . Primero, no está pasando una función lambda a la versión del filtro, lo que la hace predeterminada para la función de identidad. Al definir if not Noneen la lista de comprensión, está definiendo una función lambda (observe la MAKE_FUNCTIONdeclaración). En segundo lugar, 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 del microbenchmarking es inútil. ¡Son un millón de iteraciones, multiplicado por mil elementos! La diferencia es insignificante . 17/01/19 a las 9:27
  • list(filter(None, seq))es igual a [i for i in seq if i]no i is not None. docs.python.org/3/library/functions.html#filter 23 mar a las 13:40
1

Resumiendo otras respuestas

Al examinar las respuestas, hemos visto muchas cosas de ida y vuelta, ya sea que la comprensión de la lista o el filtro sean más rápidos o si es incluso importante o pitónico preocuparse por este tema. Al final, la respuesta es como la mayoría de las veces: depende.

Me encontré con esta pregunta mientras optimizaba el código donde esta pregunta exacta (aunque combinada con una inexpresión, no ==) es muy relevante: la expresión filter+ lambdaestá ocupando un tercio de mi tiempo de cálculo (de varios minutos).

Mi caso

En mi caso, la comprensión de la lista es mucho más rápida (el doble de velocidad). Pero sospecho que esto varía mucho según la expresión de filtro y el intérprete de Python utilizado.

Pruébelo usted mismo

Aquí hay un fragmento de código simple que debería ser fácil de adaptar. Si lo perfila (la mayoría de los IDE pueden hacerlo fácilmente), podrá decidir fácilmente cuál es la mejor opción para su caso específico:

whitelist = set(range(0, 100000000, 27))

input_list = list(range(0, 100000000))

proximal_list = list(filter(
        lambda x: x in whitelist,
        input_list
    ))

proximal_list2 = [x for x in input_list if x in whitelist]

print(len(proximal_list))
print(len(proximal_list2))

Si no tiene un IDE que le permita crear perfiles fácilmente, intente esto en su lugar (extraído de mi código base, por lo que es un poco más complicado). Este fragmento de código creará un perfil para usted que puede visualizar fácilmente usando, por ejemplo, snakeviz :

import cProfile
from time import time


class BlockProfile:
    def __init__(self, profile_path):
        self.profile_path = profile_path
        self.profiler = None
        self.start_time = None

    def __enter__(self):
        self.profiler = cProfile.Profile()
        self.start_time = time()
        self.profiler.enable()

    def __exit__(self, *args):
        self.profiler.disable()
        exec_time = int((time() - self.start_time) * 1000)
        self.profiler.dump_stats(self.profile_path)


whitelist = set(range(0, 100000000, 27))
input_list = list(range(0, 100000000))

with BlockProfile("/path/to/create/profile/in/profile.pstat"):
    proximal_list = list(filter(
            lambda x: x in whitelist,
            input_list
        ))

    proximal_list2 = [x for x in input_list if x in whitelist]

print(len(proximal_list))
print(len(proximal_list2))
-6

Mi toma

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
1
  • 3
    inunca se dijo que fuera un dict, y no es necesario limit. Aparte de eso, ¿en qué se diferencia esto de lo que sugirió el OP y cómo responde a la pregunta?
    user707650
    9 de enero de 2014 a las 11:16