comprensione della lista vs. lambda + filtro


859

Mi è capitato di trovarmi con un'esigenza di filtraggio di base: ho un elenco e devo filtrarlo per un attributo degli elementi.

Il mio codice era simile al seguente:

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

Ma poi ho pensato, non sarebbe meglio scriverlo in questo modo?

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

È più leggibile e, se necessario per le prestazioni, il lambda potrebbe essere rimosso per ottenere qualcosa.

La domanda è: ci sono avvertenze nell'usare il secondo modo? Qualche differenza di prestazioni? Mi manca del tutto Pythonic Way ™ e dovrei farlo in un altro modo (come usare itemgetter anziché lambda)?


19
Un esempio migliore sarebbe un caso in cui avevi già una funzione ben definita da usare come predicato. In quel caso, penso che molte più persone sarebbero d'accordo sul fatto che filterfosse più leggibile. Quando hai un'espressione semplice che può essere usata così com'è in un listcomp, ma deve essere racchiusa in un lambda (o similmente costruito da partialo operatorfunzioni, ecc.) Per passare filter, ecco quando vincono i listcomps.
abarnert,

3
Va detto che almeno in Python3, il ritorno di filterè un oggetto generatore di filtro e non un elenco.
Matteo Ferla,

Risposte:


589

È strano quanta bellezza varia per le diverse persone. Trovo che la comprensione dell'elenco sia molto più chiara di filter+ lambda, ma uso quello che ritieni più semplice.

Ci sono due cose che potrebbero rallentarne l'uso filter.

Il primo è l'overhead della chiamata di funzione: non appena si utilizza una funzione Python (creata da defo lambda), è probabile che il filtro sia più lento della comprensione dell'elenco. Quasi sicuramente non è abbastanza importante, e non dovresti pensare molto alle prestazioni finché non hai cronometrato il tuo codice e non hai trovato un collo di bottiglia, ma la differenza ci sarà.

L'altro overhead che potrebbe essere applicato è che lambda è costretta ad accedere a una variabile con ambito ( value). È più lento dell'accesso a una variabile locale e in Python 2.x la comprensione dell'elenco accede solo alle variabili locali. Se stai usando Python 3.x la comprensione dell'elenco viene eseguita in una funzione separata, quindi accederà anchevalue attraverso una chiusura e questa differenza non verrà applicata.

L'altra opzione da considerare è usare un generatore invece di una comprensione della lista:

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

Quindi nel tuo codice principale (che è dove la leggibilità conta davvero) hai sostituito sia la comprensione dell'elenco che il filtro con un nome di funzione che si spera abbia significato.


68
+1 per il generatore. Ho un link a casa a una presentazione che mostra quanto possano essere straordinari generatori. Puoi anche sostituire la comprensione dell'elenco con un'espressione del generatore semplicemente cambiando []in (). Inoltre, sono d'accordo che l'elenco comp è più bello.
Wayne Werner,

1
In realtà, nessun filtro è più veloce. Basta eseguire un paio di punti di riferimento rapida utilizzando qualcosa di simile stackoverflow.com/questions/5998245/...
skqr

2
@skqr è meglio usare solo timeit per i benchmark, ma per favore fai un esempio in cui ritieni filterdi essere più veloce usando una funzione di callback Python.
Duncan,

8
@ tnq177 È la presentazione di David Beasley sui generatori - dabeaz.com/generators
Wayne Werner,

2
@ VictorSchröder sì, forse non ero chiaro. Quello che stavo cercando di dire era che nel codice principale devi essere in grado di vedere l'immagine più grande. Nella piccola funzione di aiuto devi solo preoccuparti di quella funzione, cos'altro può succedere all'esterno può essere ignorato.
Duncan,

237

Questo è un problema un po 'religioso in Python. Anche se Guido ha considerato la rimozione map, filtere reduceda Python 3 , c'era abbastanza di una reazione che alla fine è reducestata spostata solo dai built-in a functools.reduce .

Personalmente trovo più comprensibile la comprensione delle liste. È più esplicito ciò che sta accadendo dall'espressione [i for i in list if i.attribute == value]poiché tutto il comportamento è in superficie, non all'interno della funzione di filtro.

Non mi preoccuperei troppo della differenza di prestazioni tra i due approcci in quanto è marginale. Lo ottimizzerei davvero solo se si rivelasse il collo di bottiglia nella tua applicazione, il che è improbabile.

Anche dal momento che il BDFL voleva essere filteruscito dal linguaggio, sicuramente questo rendeva automaticamente più comprensibili le liste Pythonic ;-)


1
Grazie per il link per inserire di Guido, se non altro per me vuol dire che cercherò di non usarli più, in modo che io non otterrà l'abitudine, e non voglio diventare solidale di quella religione :)
dashesy

1
ma ridurre è il più complesso da fare con strumenti semplici! mappa e filtro sono banali da sostituire con la comprensione!
njzk2,

8
non sapevo che ridurre era stato retrocesso in Python3. grazie per la comprensione! riducono () è ancora abbastanza utile nel calcolo distribuito, come PySpark. Penso che sia stato un errore ...
Tagar,

1
@Tagar puoi ancora usare riduci devi solo importarlo da functools
icc97

69

Dal momento che qualsiasi differenza di velocità è destinata a essere minuscola, se usare i filtri o elencare le conoscenze dipende da una questione di gusti. In generale, sono propenso a usare le comprensioni (che sembrano concordare con la maggior parte delle altre risposte qui), ma c'è un caso in cui preferisco filter.

Un caso d'uso molto frequente è quello di estrarre i valori di alcuni X iterabili soggetti a un predicato P (x):

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

ma a volte vuoi applicare prima qualche funzione ai valori:

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


Come esempio specifico, considera

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

Penso che questo appaia leggermente meglio dell'uso filter. Ma ora considera

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

In questo caso vogliamo filtercontro il valore post-calcolato. Oltre al problema di calcolare il cubo due volte (immagina un calcolo più costoso), c'è il problema di scrivere l'espressione due volte, violando l' estetica ASCIUTTA . In questo caso sarei pronto ad usare

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

7
Non prenderesti in considerazione l'uso del primo tramite un'altra comprensione dell'elenco? Come ad esempio[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9,

20
x*x*xnon può essere un numero primo, come ha x^2e xcome fattore, l'esempio non ha davvero senso in modo matematico, ma forse è ancora utile. (Forse potremmo trovare qualcosa di meglio comunque?)
Zelphir Kaltstahl,

3
Nota che potremmo usare un'espressione di generatore invece per l'ultimo esempio se non vogliamo consumare memoria:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq

4
@MateenUlhaq può essere ottimizzato per prime_cubes = [1]salvare sia i cicli di memoria che quelli della CPU ;-)
Dennis Krupenik,

7
@DennisKrupenik O meglio,[]
Mateen Ulhaq,

29

Anche se filterpuò essere il "modo più veloce", il "modo Pythonic" non sarebbe preoccuparsi di tali cose a meno che le prestazioni non siano assolutamente critiche (nel qual caso non useresti Python!).


10
Commento tardivo a un argomento spesso visto: a volte fa la differenza avere un'analisi eseguita in 5 ore invece di 10, e se ciò può essere ottenuto prendendo un'ora ottimizzando il codice Python, può valerne la pena (specialmente se uno è a proprio agio con Python e non con linguaggi più veloci).
bli,

Ma più importante è quanto il codice sorgente ci rallenta nel tentativo di leggerlo e comprenderlo!
thoni56,

20

Ho pensato di aggiungere che in Python 3, filter () è in realtà un oggetto iteratore, quindi dovresti passare la tua chiamata del metodo di filtro a list () per costruire l'elenco filtrato. Quindi in 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)

gli elenchi bec hanno gli stessi valori e sono stati completati all'incirca nello stesso momento in cui filter () era equivalente [x per x in y se z]. Tuttavia, in 3, questo stesso codice lascerebbe l'elenco c contenente un oggetto filtro, non un elenco filtrato. Per produrre gli stessi valori in 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))

Il problema è che list () accetta un iterabile come argomento e crea un nuovo elenco da quell'argomento. Il risultato è che l'uso del filtro in questo modo in Python 3 richiede fino al doppio del metodo [x per x in y se z] perché è necessario scorrere l'output di filter () e l'elenco originale.


13

Una differenza importante è che la comprensione dell'elenco restituirà un listpo 'di tempo mentre il filtro restituisce a filter, che non è possibile manipolare come un list(ovvero: chiamare lensu di esso, che non funziona con il ritorno di filter).

Il mio autoapprendimento mi ha portato ad un problema simile.

Detto questo, se c'è un modo per ottenere il risultato listda un filter, un po 'come faresti in .NET quando lo fai lst.Where(i => i.something()).ToList(), sono curioso di saperlo.

EDIT: questo è il caso di Python 3, non 2 (vedi discussione nei commenti).


4
filter restituisce un elenco e su di esso possiamo usare len. Almeno nel mio Python 2.7.6.
thiruvenkadam,

7
Non è il caso di 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
"se c'è un modo per avere l'elenco risultante ... sono curioso di conoscerlo". Basta chiamare list()sul risultato: list(filter(my_func, my_iterable)). E ovviamente potresti sostituirlo listcon set, o tuple, o qualsiasi altra cosa che richieda un iterabile. Ma per chiunque non sia programmatore funzionale, il caso è ancora più forte usare una comprensione dell'elenco piuttosto che filteruna conversione esplicita list.
Steve Jessop,

10

Trovo il secondo modo più leggibile. Ti dice esattamente qual è l'intenzione: filtrare l'elenco.
PS: non usare 'list' come nome di variabile


7

generalmente filterè leggermente più veloce se si utilizza una funzione integrata.

Mi aspetto che la comprensione dell'elenco sia leggermente più veloce nel tuo caso


python -m timeit 'filter (lambda x: x in [1,2,3,4,5], range (10000000))' 10 loop, meglio di 3: 1,44 sec per loop python -m timeit '[x per x nel range (10000000) se x in [1,2,3,4,5]] '10 loop, meglio di 3: 860 msec per loop Non proprio ?!
giaosudau,

@sepdau, le funzioni lambda non sono integrate. La comprensione dell'elenco è migliorata negli ultimi 4 anni - ora la differenza è comunque trascurabile anche con le funzioni integrate
John La Rooy,

7

Il filtro è proprio questo. Filtra gli elementi di un elenco. Puoi vedere la stessa menzione della definizione (nel link ufficiale ai documenti che ho menzionato prima). Considerando che la comprensione dell'elenco è qualcosa che produce un nuovo elenco dopo aver agito su qualcosa dell'elenco precedente (sia la comprensione del filtro che dell'elenco crea un nuovo elenco e non esegue operazioni al posto dell'elenco precedente. Un nuovo elenco qui è qualcosa come un elenco con , diciamo, un tipo di dati completamente nuovo. Come convertire interi in string, ecc.)

Nel tuo esempio, è meglio usare il filtro piuttosto che la comprensione dell'elenco, come da definizione. Tuttavia, se vuoi, dì altro_attributo dagli elementi dell'elenco, nel tuo esempio deve essere recuperato come nuovo elenco, quindi puoi usare la comprensione dell'elenco.

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

Questo è il modo in cui ricordo davvero la comprensione di filtri ed elenchi. Rimuovi alcune cose da un elenco e mantieni intatti gli altri elementi, usa il filtro. Usa un po 'di logica per gli elementi e crea un elenco annacquato adatto per qualche scopo, usa la comprensione dell'elenco.


2
Sarò felice di conoscere il motivo del voto negativo in modo da non ripeterlo in futuro.
thiruvenkadam,

la definizione di filtro e la comprensione dell'elenco non erano necessarie, poiché il loro significato non veniva discusso. Che una comprensione di lista dovrebbe essere usata solo per "nuove" liste è presentata ma non contestata.
Agos,

Ho usato la definizione per dire che il filtro ti dà la lista con gli stessi elementi che sono veri per un caso ma con la comprensione della lista possiamo modificare gli elementi stessi, come convertire int in str. Ma punto preso :-)
thiruvenkadam,

4

Ecco un breve pezzo che uso quando devo filtrare qualcosa dopo la comprensione dell'elenco. Solo una combinazione di filtro, lambda ed elenchi (altrimenti noto come la lealtà di un gatto e la pulizia di un cane).

In questo caso sto leggendo un file, rimuovendo le righe vuote, commentando le righe e qualsiasi cosa dopo un commento su una riga:

# 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])]

Questo in effetti ottiene molto in pochissimo codice. Penso che potrebbe essere un po 'troppa logica in una riga per capire facilmente e la leggibilità è ciò che conta però.
Zelphir Kaltstahl,

Potresti scrivere questo comefile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop il

4

Oltre alla risposta accettata, esiste un caso angolare in cui è necessario utilizzare il filtro anziché una comprensione dell'elenco. Se l'elenco non è lavabile, non è possibile elaborarlo direttamente con una comprensione dell'elenco. Un esempio del mondo reale è se si utilizza pyodbcper leggere i risultati da un database. Il fetchAll()risultato cursorè un elenco non lavabile. In questa situazione, per manipolare direttamente i risultati restituiti, è necessario utilizzare il 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) 

Se usi la comprensione dell'elenco qui otterrai l'errore:

TypeError: tipo non lavabile: 'list'


1
tutte le liste non sono lavabili in >>> hash(list()) # TypeError: unhashable type: 'list'secondo luogo, questo funziona benissimo:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger

"Se l'elenco non è lavabile, non è possibile elaborarlo direttamente con una comprensione dell'elenco." Questo non è vero e tutti gli elenchi sono comunque non lavabili.
juanpa.arrivillaga,

3

Mi ci è voluto del tempo per familiarizzare con il higher order functions filtere map. Quindi mi sono abituato a loro e in realtà mi è piaciuto filterdato che era esplicito che filtra mantenendo tutto ciò che è vero e mi sono sentito bene a conoscere alcuni functional programmingtermini.

Quindi ho letto questo brano (Fluent Python Book):

Le funzioni di mappa e filtro sono ancora integrate in Python 3, ma dall'introduzione delle comprensioni di elenchi e delle espressioni di generatori, non sono così importanti. Un listcomp o un genexp fa il lavoro di mappa e filtro combinati, ma è più leggibile.

E ora penso, perché preoccuparsi del concetto di filter/ mapse riesci a realizzarlo con modi di dire già ampiamente diffusi come la comprensione delle liste. Inoltre mapse filterssono tipo di funzioni. In questo caso preferisco usareAnonymous functions lambdas.

Infine, solo per il gusto di averlo testato, ho cronometrato entrambi i metodi ( mape listComp) e non ho visto alcuna differenza di velocità rilevante che giustificherebbe argomentazioni al riguardo.

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

0

Curiosamente su Python 3, vedo il filtro funzionare più velocemente della comprensione dell'elenco.

Ho sempre pensato che la comprensione della lista sarebbe stata più performante. Qualcosa del tipo: [nome per nome in brand_names_db se il nome non è Nessuno] Il bytecode generato è leggermente migliore.

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

Ma in realtà sono più lenti:

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

8
Confronto non valido . Innanzitutto, non si passa una funzione lambda alla versione del filtro, che la rende predefinita per la funzione identità. Quando si definisce if not Nonenella comprensione dell'elenco si sta definendo una funzione lambda (notare l' MAKE_FUNCTIONaffermazione). In secondo luogo, i risultati sono diversi, poiché la versione di comprensione dell'elenco rimuoverà solo il Nonevalore, mentre la versione del filtro rimuoverà tutti i valori "falsi". Detto questo, l'intero scopo del microbenchmarking è inutile. Quelle sono un milione di iterazioni, volte 1k articoli! La differenza è trascurabile .
Victor Schröder,

-7

La mia opinione

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

3
inon è mai stato detto di essere un dicte non ce n'è bisogno limit. Oltre a ciò, in che modo differisce da quanto suggerito dall'OP e come risponde alla domanda?
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.