it-swarm.com.de

listenverständnis vs. Lambda + Filter

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

Mein Code sah so aus:

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

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

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

Es ist lesbarer und wenn nötig, kann der Lambda herausgenommen werden, um etwas zu erreichen. 

Die Frage ist: Gibt es irgendwelche Vorbehalte bei der Verwendung der zweiten Möglichkeit? Irgendein Leistungsunterschied? Verpasse ich den Pythonic Way ™ gänzlich und sollte es auf eine andere Weise tun (z. B. itemgetter anstelle von Lambda)?

703
Agos

Es ist seltsam, wie viel Schönheit für verschiedene Menschen variiert. Ich finde das Listenverständnis viel klarer als filter + lambda, benutze aber das, was Sie einfacher finden. Hören Sie jedoch auf, Ihren Variablen Namen zu geben, die bereits für integrierte Elemente verwendet wurden. Das ist verwirrend. [ Die Frage verwendete ursprünglich list als Variablennamen, wurde jedoch als Antwort auf diese Antwort aktualisiert. ]

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

Der erste ist der Funktionsaufruf-Overhead: Sobald Sie eine Python-Funktion verwenden (unabhängig davon, ob sie mit def oder lambda erstellt wurde), ist der Filter wahrscheinlich langsamer als das Listenverständnis. Es ist fast sicher nicht genug, um eine Rolle zu spielen, und Sie sollten nicht viel über die Leistung nachdenken, bis Sie Ihren Code zeitlich festgelegt und einen Engpass für ihn gefunden haben. Der Unterschied wird jedoch bestehen.

Der andere zusätzliche Aufwand ist, dass das Lambda gezwungen ist, auf eine Bereichsvariable (value) zuzugreifen. Das ist langsamer als beim 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 es auch über eine Schließung auf value zugreift 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 (in dem Lesbarkeit wirklich wichtig ist) sowohl das Listenverständnis als auch den Filter durch einen hoffentlich aussagekräftigen Funktionsnamen ersetzt.

486
Duncan

Dies ist ein etwas religiöses Thema in Python. Obwohl Guido die Entfernung von map, filter und reduce aus Python 3 in Betracht zog, gab es genug Spiel, dass am Ende nur reduce von eingebauten in functools.reduce verschoben wurde.

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

Ich würde mich nicht zu sehr um die Leistungsunterschiede zwischen den beiden Ansätzen sorgen, da dies marginal ist. Ich würde dies wirklich nur optimieren, wenn sich der Engpass in Ihrer Anwendung als unwahrscheinlich erwies.

Auch da der BDFLfilter von der Sprache weg wollte, macht das sicherlich Listenverständnisse automatisch mehr Pythonic ;-)

201
Tendayi Mawushe

Da jeder Geschwindigkeitsunterschied klein sein muss, kommt es auf den Geschmack an, ob Filter oder Listenverständnisse verwendet werden. Im Allgemeinen neige ich dazu, Verständniss zu verwenden (was hier mit den meisten anderen Antworten übereinstimmt), aber es gibt einen Fall, in dem ich filter vorziehen möchte. 

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

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

aber manchmal möchten Sie zuerst eine Funktion auf die Werte anwenden:

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


Betrachten Sie als ein spezielles Beispiel

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

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

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

In diesem Fall möchten wir filter gegen den nachberechneten Wert. Neben der Frage, den Cube zweimal zu berechnen (stellen Sie sich eine teurere Berechnung vor), gibt es auch das Problem, den Ausdruck zweimal zu schreiben, wodurch die Ästhetik von DRY verletzt wird. In diesem Fall wäre ich geeignet zu verwenden

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
60
I. J. Kennedy

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

27
Umang

Ich dachte, ich würde einfach hinzufügen, dass filter () in python 3 eigentlich ein Iterator-Objekt ist. Sie müssen also den Aufruf der filter-Methode an list () übergeben, 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 die gleichen Werte und wurden ungefähr in derselben Zeit abgeschlossen, in der 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 und keine gefilterte Liste enthält. Um die gleichen Werte in 3 zu erzeugen:

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 () ein iterierbares Argument als Argument verwendet und aus diesem Argument eine neue Liste erstellt. Das Ergebnis ist, dass die Verwendung des Filters auf diese Weise in Python 3 bis zu doppelt so lange dauert wie die Methode [x für x in y if z], da Sie sowohl die Ausgabe von filter () als auch die ursprüngliche Liste durchlaufen müssen. 

15
Jim50

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

Mein eigenes Selbstlernen brachte mich zu einem ähnlichen Problem.

Das heißt, wenn es eine Möglichkeit gibt, die list von einer filter zu erhalten, etwas, wie Sie es in .NET tun würden, wenn Sie lst.Where(i => i.something()).ToList() tun, bin ich neugierig, es zu wissen.

BEARBEITEN: Dies ist der Fall für Python 3, nicht für 2 (siehe die Diskussion in den Kommentaren).

10
Adeynack

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

9
unbeli

Filter ist genau das. Es filtert die Elemente einer Liste heraus. Sie können sehen, dass die Definition dasselbe erwähnt (im Link zu den offiziellen Dokumenten, den ich zuvor erwähnt habe). Das Listenverständnis hingegen erzeugt eine neue Liste, nachdem auf etwas in der vorherigen Liste eingegangen wurde (sowohl das Filter- als auch das Listenverständnis erstellt eine neue Liste und führt keine Operation anstelle der älteren Liste aus. Eine neue Liste ist hier etwas wie eine Liste mit einem völlig neuen Datentyp (wie das Konvertieren von Ganzzahlen in Zeichenfolgen usw.)

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

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

So erinnere ich mich eigentlich an das Filter- und Listenverständnis. Entfernen Sie ein paar Elemente aus einer Liste und behalten Sie die anderen Elemente bei, verwenden Sie einen Filter. Verwenden Sie eine eigene Logik bei den Elementen und erstellen Sie eine verwässerte Liste, die für einen bestimmten Zweck geeignet ist, und verwenden Sie das Listenverständnis.

7
thiruvenkadam

im Allgemeinen ist filter etwas schneller, wenn eine integrierte Funktion verwendet wird.

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

6
John La Rooy

Hier ist ein kurzes Stück, das ich verwende, wenn ich nach etwas filtern muss, after dem Listenverständnis. 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 leere Zeilen, auskommentierte Zeilen und alles nach einem Kommentar zu 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])]
5
rharder

Neben der akzeptierten Antwort gibt es einen Eckpunkt, in dem Sie Filter anstelle von Listenverständnis verwenden sollten. Wenn die Liste nicht unverschiebbar ist, können Sie sie nicht direkt mit einem Listenverständnis bearbeiten. Ein reales Beispiel ist, wenn Sie pyodbc verwenden, um Ergebnisse aus einer Datenbank zu lesen. Die fetchAll()-Ergebnisse von cursor sind eine unhashable Liste. In dieser Situation sollte der 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, erhalten Sie den Fehler:

TypeError: unhashable type: 'list'

4
C.W.praen

Ich habe einige Zeit gebraucht, um mich mit den higher order functionsfilter und map vertraut zu machen. Also habe ich mich an sie gewöhnt und eigentlich mochte ich filter, da es explizit war, dass es gefiltert wird, indem es die Wahrheit behält und ich mich cool fühlte, dass ich einige functional programming-Ausdrücke kannte. 

Dann las ich diese Passage (Fluent Python Book): 

Die Karten- und Filterfunktionen sind immer noch eingebaut in Python 3, aber seit der Einführung von Listenverständnissen und Generator ex ‐ Drücke sind sie nicht so wichtig. Ein listcomp oder ein genexp erledigt map und Filter kombiniert, ist aber lesbarer. 

Und jetzt denke ich, warum sollte man sich mit dem Konzept von filter/map beschäftigen, wenn man es mit bereits weit verbreiteten Idiomen wie Listenverständnissen erreichen kann. Außerdem sind maps und filters eine Art Funktion. In diesem Fall bevorzuge ich Anonymous functions Lambdas. 

Schließlich, nur um es testen zu lassen, habe ich beide Methoden zeitlich festgelegt (map und listComp) und ich habe keinen relevanten Geschwindigkeitsunterschied gesehen, der es rechtfertigt, Argumente darüber zu machen. 

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

Neugierig auf Python 3 sehe ich, dass der Filter schneller arbeitet als Listenverständnisse.

Ich dachte immer, die Listenkomplexe wären performanter . Etwas wie: [Name für Name in brand_names_db, wenn name nicht None ist.

>>> 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
0
Rod Senra