Listenverständnis vs. Lambda + Filter

857

Ich hatte zufällig ein grundlegendes Filterbedürfnis: Ich habe eine Liste und muss sie nach einem Attribut der Elemente filtern.

Mein Code sah folgendermaßen aus:

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

Aber dann dachte ich, wäre es nicht besser, es so zu schreiben?

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

Es ist besser lesbar, und wenn es für die Leistung benötigt wird, kann das Lambda herausgenommen werden, um etwas zu gewinnen.

Die Frage ist: Gibt es irgendwelche Einschränkungen bei der Verwendung des zweiten Weges? Leistungsunterschiede? Vermisse ich den Pythonic Way ™ vollständig und sollte es auf eine andere Weise tun (z. B. mit Itemgetter anstelle des Lambda)?

Agos
quelle
19
Ein besseres Beispiel wäre ein Fall, in dem Sie bereits eine gut benannte Funktion als Prädikat hatten. In diesem Fall würden meiner Meinung nach viel mehr Menschen zustimmen, dass dies filterbesser lesbar ist. Wenn Sie einen einfachen Ausdruck haben, der unverändert in einem Listencomputer verwendet werden kann, aber in ein Lambda (oder ein ähnliches Konstrukt aus partialoder operatorFunktionen usw.) eingeschlossen werden muss, an das übergeben werden filtersoll, dann gewinnen Listcomps.
Abarnert
3
Es sollte gesagt werden, dass zumindest in Python3 die Rückgabe von filterein Filtergeneratorobjekt ist, keine Liste.
Matteo Ferla

Antworten:

588

Es ist seltsam, wie unterschiedlich die Schönheit für verschiedene Menschen ist. Ich finde das Listenverständnis viel klarer als filter+ lambda, benutze aber das, was du leichter findest.

Es gibt zwei Dinge, die Ihre Verwendung verlangsamen können filter.

Der erste ist der Overhead des Funktionsaufrufs: Sobald Sie eine Python-Funktion verwenden (unabhängig davon, ob sie von defoder erstellt wurde lambda), ist der Filter wahrscheinlich langsamer als das Listenverständnis. Es ist mit ziemlicher Sicherheit nicht genug, um eine Rolle zu spielen, und Sie sollten nicht viel über die Leistung nachdenken, bis Sie Ihren Code zeitlich festgelegt und festgestellt haben, dass es sich um einen Engpass handelt, aber der Unterschied wird da sein.

Der andere Aufwand, der möglicherweise anfällt, besteht darin, dass das Lambda gezwungen wird, auf eine Gültigkeitsbereichsvariable ( value) zuzugreifen . Das ist langsamer als der Zugriff auf eine lokale Variable und in Python 2.x greift das Listenverständnis nur auf lokale Variablen zu. Wenn Sie Python 3.x verwenden, wird das Listenverständnis in einer separaten Funktion ausgeführt, sodass der Zugriff auch valueüber einen Abschluss erfolgt und dieser Unterschied nicht gilt.

Die andere zu berücksichtigende Option ist die Verwendung eines Generators anstelle eines Listenverständnisses:

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

Dann haben Sie in Ihrem Hauptcode (wo die Lesbarkeit wirklich wichtig ist) sowohl das Listenverständnis als auch den Filter durch einen hoffentlich aussagekräftigen Funktionsnamen ersetzt.

Duncan
quelle
68
+1 für den Generator. Ich habe zu Hause einen Link zu einer Präsentation, die zeigt, wie großartig Generatoren sein können. Sie können auch die Liste Verständnis mit einem Generator Ausdruck ersetzen Sie einfach durch Änderung []zu (). Ich stimme auch zu, dass die Liste comp schöner ist.
Wayne Werner
1
Eigentlich ist kein Filter schneller. Führen Sie einfach ein paar schnelle Benchmarks mit etwas wie stackoverflow.com/questions/5998245/…
skqr
2
@skqr ist besser, nur timeit für Benchmarks zu verwenden, aber bitte geben Sie ein Beispiel, in dem Sie filtermithilfe einer Python-Rückruffunktion feststellen , dass Sie schneller sind.
Duncan
8
@ tnq177 Es ist David Beasleys Präsentation über Generatoren - dabeaz.com/generators
Wayne Werner
2
@ VictorSchröder ja, vielleicht war ich unklar. Ich wollte damit sagen, dass Sie im Hauptcode das Gesamtbild sehen müssen. In der kleinen Hilfsfunktion müssen Sie sich nur um diese eine Funktion kümmern, was sonst noch draußen vor sich geht, kann ignoriert werden.
Duncan
237

Dies ist ein etwas religiöses Thema in Python. Obwohl Guido überlegte, es zu entfernen map, filterund reduceaus Python 3 , gab es genug Spiel, das am Ende nur reducevon eingebauten zu functools.reduce verschoben wurde .

Persönlich finde ich Listenverständnisse leichter zu lesen. Es ist expliziter, was mit dem Ausdruck geschieht, [i for i in list if i.attribute == value]da sich das gesamte Verhalten auf der Oberfläche befindet und nicht innerhalb der Filterfunktion.

Ich würde mir keine Sorgen über den Leistungsunterschied zwischen den beiden Ansätzen machen, da dieser marginal ist. Ich würde dies wirklich nur optimieren, wenn sich herausstellen würde, dass der Engpass in Ihrer Anwendung unwahrscheinlich ist.

Auch da die BDFL dannfilter aus der Sprache verschwunden sein wollte , macht das Listenverständnis sicherlich automatisch pythonischer ;-)

Tendayi Mawushe
quelle
1
Vielen Dank für die Links zu Guidos Beitrag, wenn nichts anderes für mich bedeutet, dass ich versuchen werde, sie nicht mehr zu verwenden, damit ich nicht die Gewohnheit bekomme und diese Religion nicht unterstütze :)
dashesy
1
Reduzieren ist jedoch am komplexesten mit einfachen Werkzeugen! Karte und Filter sind trivial, um sie durch Verständnis zu ersetzen!
NJZK2
8
Ich wusste nicht, dass Reduzieren in Python3 herabgestuft wurde. danke für den einblick! reduct () ist in verteilten Computern wie PySpark immer noch sehr hilfreich. Ich denke, das war ein Fehler.
Tagar
1
@Tagar können Sie immer noch reduzieren verwenden Sie müssen es nur von functools importieren
icc97
69

Da jeder Geschwindigkeitsunterschied zwangsläufig winzig ist, hängt es vom Geschmack ab, ob Filter oder Listenverständnisse verwendet werden. Im Allgemeinen neige ich dazu, Verständnis zu verwenden (was mit den meisten anderen Antworten hier übereinzustimmen scheint), aber es gibt einen Fall, in dem ich es vorziehen würde filter.

Ein sehr häufiger Anwendungsfall ist das Herausziehen der Werte eines iterierbaren X, das einem Prädikat P (x) unterliegt:

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

Manchmal möchten Sie jedoch zuerst eine Funktion auf die Werte anwenden:

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


Betrachten Sie als konkretes Beispiel

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

Ich denke, das sieht etwas besser aus als zu verwenden filter. Aber jetzt überlegen Sie

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

In diesem Fall wollen wir filtergegen den nachberechneten Wert. Neben dem Problem, den Würfel zweimal zu berechnen (stellen Sie sich eine teurere Berechnung vor), gibt es das Problem, den Ausdruck zweimal zu schreiben, was die DRY- Ästhetik verletzt . In diesem Fall würde ich gerne verwenden

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
IJ Kennedy
quelle
7
Würden Sie nicht in Betracht ziehen, die Primzahl über ein anderes Listenverständnis zu verwenden? Wie[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9
20
x*x*xkann keine Primzahl sein, wie es ist x^2und xals Faktor macht das Beispiel auf mathematische Weise keinen Sinn, aber vielleicht ist es immer noch hilfreich. (Vielleicht könnten wir aber etwas Besseres finden?)
Zelphir Kaltstahl
3
Beachten Sie, dass wir stattdessen einen Generatorausdruck für das letzte Beispiel verwenden können, wenn wir den Speicher nicht auffressen möchten:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq
4
@MateenUlhaq dies kann optimiert werden, prime_cubes = [1]um sowohl Speicher- als auch CPU-Zyklen zu sparen
;-)
7
@ TennisKrupenik Oder besser gesagt,[]
Mateen Ulhaq
29

Obwohl filterdies der "schnellere Weg" sein mag, wäre der "pythonische Weg", sich nicht um solche Dinge zu kümmern, es sei denn, die Leistung ist absolut kritisch (in diesem Fall würden Sie Python nicht verwenden!).

Umang
quelle
9
Später Kommentar zu einem oft gesehenen Argument: Manchmal macht es einen Unterschied, wenn eine Analyse in 5 statt in 10 Stunden ausgeführt wird. Wenn dies durch eine Stunde Optimierung des Python-Codes erreicht werden kann, kann es sich lohnen (insbesondere, wenn dies der Fall ist) bequem mit Python und nicht mit schnelleren Sprachen).
bli
Wichtiger ist jedoch, wie sehr uns der Quellcode beim Lesen und Verstehen verlangsamt!
Thoni56
20

Ich dachte, ich würde nur hinzufügen, dass filter () in Python 3 tatsächlich ein Iteratorobjekt ist, sodass Sie Ihren Filtermethodenaufruf an list () übergeben müssen, um die gefilterte Liste zu erstellen. Also 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)

Die Listen b und c haben dieselben Werte und wurden ungefähr zur gleichen Zeit abgeschlossen, als filter () äquivalent war [x für x in y, wenn z]. In 3 würde derselbe Code jedoch die Liste c verlassen, die ein Filterobjekt enthält, keine gefilterte Liste. So erzeugen Sie die gleichen Werte 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))

Das Problem ist, dass list () eine iterable als Argument verwendet und aus diesem Argument eine neue Liste erstellt. Das Ergebnis ist, dass die Verwendung von Filter auf diese Weise in Python 3 bis zu doppelt so lange dauert wie die Methode [x für x in y wenn z], da Sie sowohl die Ausgabe von filter () als auch die ursprüngliche Liste durchlaufen müssen.

Jim50
quelle
13

Ein wichtiger Unterschied besteht darin, dass das Listenverständnis eine listWeile zurückgibt filter, während der Filter ein zurückgibt , das Sie nicht wie ein manipulieren können list(dh: Rufen Sie lenes auf, was mit der Rückgabe von nicht funktioniert filter).

Mein eigenes Selbstlernen brachte mich zu einem ähnlichen Thema.

Abgesehen davon bin ich neugierig , wenn es eine Möglichkeit gibt, das Ergebnis listvon a zu erhalten filter, ähnlich wie Sie es in .NET tun würden lst.Where(i => i.something()).ToList(), wenn Sie dies tun .

BEARBEITEN: Dies ist bei Python 3 der Fall, nicht bei 2 (siehe Diskussion in den Kommentaren).

Adeynack
quelle
4
Filter gibt eine Liste zurück und wir können len darauf verwenden. Zumindest in meinem Python 2.7.6.
Thiruvenkadam
7
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'>
Dies
3
"Wenn es einen Weg gibt, die resultierende Liste zu haben ... bin ich neugierig, es zu wissen". Rufen Sie einfach list()das Ergebnis auf : list(filter(my_func, my_iterable)). Und natürlich können Sie ersetzen listmit set, oder tuple, oder irgendetwas anderes , das eine iterable nimmt. Für andere als funktionale Programmierer ist es jedoch noch wichtiger, ein Listenverständnis zu verwenden, als eine filterexplizite Konvertierung in list.
Steve Jessop
10

Ich finde den zweiten Weg besser lesbar. Es sagt Ihnen genau, was die Absicht ist: Filtern Sie die Liste.
PS: Verwenden Sie 'Liste' nicht als Variablennamen

unbeli
quelle
7

Im Allgemeinen filterist es etwas schneller, wenn eine integrierte Funktion verwendet wird.

Ich würde erwarten, dass das Listenverständnis in Ihrem Fall etwas schneller ist

John La Rooy
quelle
python -m timeit 'Filter (Lambda x: x in [1,2,3,4,5], Bereich (10000000))' 10 Schleifen, am besten 3: 1,44 Sek. pro Schleife python -m timeit '[x für x im Bereich (10000000) wenn x in [1,2,3,4,5]] '10 Schleifen, am besten 3: 860 ms pro Schleife Nicht wirklich?!
Giaosudau
@sepdau, Lambda-Funktionen sind keine eingebauten Funktionen. Das Listenverständnis hat sich in den letzten 4 Jahren verbessert - jetzt ist der Unterschied trotz eingebauter Funktionen ohnehin vernachlässigbar
John La Rooy
7

Filter ist genau das. Es filtert die Elemente einer Liste heraus. Sie können sehen, dass die Definition dasselbe erwähnt (in dem offiziellen Dokument-Link, den ich zuvor erwähnt habe). Während das Listenverständnis etwas ist, das eine neue Liste erzeugt, nachdem es auf etwas in der vorherigen Liste reagiert hat. (Sowohl das Filter- als auch das Listenverständnis erzeugen eine neue Liste und führen keine Operation anstelle der älteren Liste aus. Eine neue Liste hier ist so etwas wie eine Liste mit B. ein völlig neuer Datentyp. Wie das Konvertieren von Ganzzahlen in Zeichenfolgen usw.)

In Ihrem Beispiel ist es besser, Filter als Listenverständnis gemäß der Definition zu verwenden. Wenn Sie jedoch möchten, dass other_attribute aus den Listenelementen in Ihrem Beispiel als neue Liste abgerufen werden soll, können Sie das Listenverständnis verwenden.

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

So erinnere ich mich tatsächlich an das Filter- und Listenverständnis. Entfernen Sie einige Dinge in einer Liste und lassen Sie die anderen Elemente intakt. Verwenden Sie den Filter. Verwenden Sie eine eigene Logik für die Elemente und erstellen Sie eine verwässerte Liste, die für einen bestimmten Zweck geeignet ist. Verwenden Sie das Listenverständnis.

Thiruvenkadam
quelle
2
Ich werde froh sein, den Grund für die Ablehnung zu kennen, damit ich ihn in Zukunft nirgendwo mehr wiederholen werde.
Thiruvenkadam
Die Definition des Filter- und Listenverständnisses war nicht erforderlich, da ihre Bedeutung nicht diskutiert wurde. Dass ein Listenverständnis nur für „neue“ Listen verwendet werden sollte, wird vorgestellt, aber nicht argumentiert.
Agos
Ich habe die Definition verwendet, um zu sagen, dass Filter Ihnen eine Liste mit denselben Elementen gibt, die für einen Fall zutreffen, aber mit Listenverständnis können wir die Elemente selbst ändern, z. B. die Konvertierung von int in str. Aber Punkt genommen :-)
Thiruvenkadam
4

Hier ist ein kurzes Stück, das ich verwende, wenn ich nach dem Listenverständnis nach etwas filtern muss . Nur eine Kombination aus Filter, Lambda und Listen (auch bekannt als die Loyalität einer Katze und die Sauberkeit eines Hundes).

In diesem Fall lese ich eine Datei, entferne Leerzeilen, kommentiere Zeilen und alles nach einem Kommentar in einer Zeile:

# 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
quelle
Dies erreicht in der Tat viel in sehr wenig Code. Ich denke, es könnte ein bisschen zu viel Logik in einer Zeile sein, um es leicht zu verstehen, und Lesbarkeit ist das, was zählt.
Zelphir Kaltstahl
Sie könnten dies schreiben alsfile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop
4

Zusätzlich zur akzeptierten Antwort gibt es einen Eckfall, in dem Sie Filter anstelle eines Listenverständnisses verwenden sollten. Wenn die Liste nicht zerlegbar ist, können Sie sie nicht direkt mit einem Listenverständnis verarbeiten. Ein Beispiel aus der Praxis ist pyodbcdas Lesen von Ergebnissen aus einer Datenbank. Das fetchAll()Ergebnis von cursorist eine nicht zerlegbare Liste. In dieser Situation sollte ein Filter verwendet werden, um die zurückgegebenen Ergebnisse direkt zu bearbeiten:

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) 

Wenn Sie hier das Listenverständnis verwenden, wird folgende Fehlermeldung angezeigt:

TypeError: nicht zerlegbarer Typ: 'list'

CWpraen
quelle
1
Alle Listen sind nicht zerlegbar. >>> hash(list()) # TypeError: unhashable type: 'list'Zweitens funktioniert dies einwandfrei :processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger
"Wenn die Liste nicht zerlegbar ist, können Sie sie nicht direkt mit einem Listenverständnis verarbeiten." Dies ist nicht wahr und alle Listen sind sowieso nicht verwischbar.
juanpa.arrivillaga
3

Ich brauchte einige Zeit, um mich mit dem higher order functions filterund vertraut zu machen map. Also filterhabe ich mich an sie gewöhnt und es hat mir wirklich gut gefallen, da es explizit war, dass es filtert, indem es das hält, was wahr ist, und ich fühlte mich cool, dass ich einige functional programmingBegriffe kannte .

Dann las ich diese Passage (Fluent Python Book):

Die Karten- und Filterfunktionen sind in Python 3 noch integriert, aber seit der Einführung von Listenverständnissen und Generatorausdrücken sind sie nicht mehr so ​​wichtig. Ein Listcomp oder ein Genexp erledigt die Aufgabe von Map und Filter zusammen, ist aber besser lesbar.

Und jetzt denke ich, warum sollte man sich mit dem Konzept beschäftigen filter/ mapwenn man es mit bereits weit verbreiteten Redewendungen wie Listenverständnis erreichen kann. Darüber hinaus mapsund filterssind Art von Funktionen. In diesem Fall bevorzuge ich Anonymous functionsLambdas.

Schließlich habe ich, nur um es testen zu lassen, beide Methoden ( mapund listComp) zeitlich festgelegt und keinen relevanten Geschwindigkeitsunterschied festgestellt, der es rechtfertigen würde, Argumente dafür vorzulegen.

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
user1767754
quelle
0

Seltsamerweise sehe ich in Python 3, dass Filter schneller arbeiten als Listenverständnisse.

Ich dachte immer, dass das Listenverständnis performanter sein würde. So etwas wie: [Name für Name in brand_names_db, wenn name nicht None ist] Der generierte Bytecode ist etwas besser.

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

Aber sie sind tatsächlich langsamer:

   >>> 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
quelle
8
Ungültiger Vergleich . Erstens übergeben Sie keine Lambda-Funktion an die Filterversion, wodurch die Identitätsfunktion standardmäßig aktiviert wird. Bei der Definition if not Nonein der Liste Begreifen Sie sind , die eine Lambda - Funktion ( man beachte die MAKE_FUNCTIONAnweisung). Zweitens sind die Ergebnisse unterschiedlich, da die Listenverständnisversion nur den NoneWert entfernt, während die Filterversion alle "falschen" Werte entfernt. Trotzdem ist der gesamte Zweck des Mikrobenchmarkings nutzlos. Das sind eine Million Iterationen, mal 1.000 Artikel! Der Unterschied ist vernachlässigbar .
Victor Schröder
-7

Meine Einstellung

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
tim
quelle
3
iwurde nie als a bezeichnet dict, und es besteht keine Notwendigkeit für limit. Wie unterscheidet sich das von dem, was das OP vorgeschlagen hat, und wie beantwortet es die Frage?