Listenverständnis ohne [] in Python

83

Beitritt zu einer Liste:

>>> ''.join([ str(_) for _ in xrange(10) ])
'0123456789'

join muss eine iterable nehmen.

Anscheinend ist joindas Argument [ str(_) for _ in xrange(10) ], und es ist ein Listenverständnis .

Schau dir das an:

>>>''.join( str(_) for _ in xrange(10) )
'0123456789'

Nun, joindas Argument ist einfach str(_) for _ in xrange(10), nein [], aber das Ergebnis ist das gleiche.

Warum? Erzeugt str(_) for _ in xrange(10)auch eine Liste oder eine iterable?

Alcott
quelle
1
Ich würde mir vorstellen, dass joindas höchstwahrscheinlich in C geschrieben ist und daher viel schneller läuft als ein Listenverständnis ... Testzeit!
Joel Cornett
Anscheinend habe ich Ihre Frage völlig falsch gelesen. Es scheint einen Generator für mich zurückzugeben ...
Joel Cornett
17
Nur eine Anmerkung: _hat keine besondere Bedeutung, es ist ein regulärer Variablenname. Es wird oft als Wegwerfname verwendet, aber dies ist nicht der Fall (Sie verwenden die Variable). Ich würde es vermeiden, es in einem Code zu verwenden (zumindest auf diese Weise).
rplnt

Antworten:

66
>>>''.join( str(_) for _ in xrange(10) )

Dies wird als Generatorausdruck bezeichnet und in PEP 289 erläutert .

Der Hauptunterschied zwischen Generatorausdrücken und Listenverständnis besteht darin, dass erstere die Liste nicht im Speicher erstellen.

Beachten Sie, dass es eine dritte Möglichkeit gibt, den Ausdruck zu schreiben:

''.join(map(str, xrange(10)))
NPE
quelle
1
Wie ich es kenne, kann ein Generator durch einen tupelartigen Ausdruck erzeugt werden, wie z ( str(_) for _ in xrange(10) ). Aber ich war verwirrt darüber, warum das ()weggelassen werden kann join, was bedeutet, dass der Code wie '' '.join ((str (_) für _ in xrange (10))) sein sollte, richtig?
Alcott
1
@Alcott Mein Verständnis von Tupeln ist, dass sie tatsächlich durch die durch Kommas getrennte Liste von Ausdrücken und nicht durch die Klammer definiert werden. Die Klammern dienen nur dazu, die Werte in einer Zuweisung visuell zu gruppieren oder die Werte tatsächlich zu gruppieren, wenn das Tupel in eine andere durch Kommas getrennte Liste wie einen Funktionsaufruf verschoben wird. Dies wird häufig durch Ausführen von Code wie demonstriert tup = 1, 2, 3; print(tup). In diesem Sinne wird durch die Verwendung forals Teil eines Ausdrucks der Generator erstellt, und die Klammern dienen nur dazu, ihn von einer falsch geschriebenen Schleife zu unterscheiden.
Eric Ed Lohmar
131

Die anderen Befragten antworteten zu Recht, dass Sie einen Generatorausdruck entdeckt hatten (dessen Notation dem Listenverständnis ähnelt, jedoch ohne die umgebenden eckigen Klammern).

Im Allgemeinen sind Genexps (wie sie liebevoll genannt werden) speichereffizienter und schneller als Listenverständnisse.

Es ist jedoch der Fall ''.join(), wird eine Liste Verständnis ist schneller und mehr Speicher effizienter zu gestalten . Der Grund dafür ist, dass der Join zwei Durchgänge über die Daten durchführen muss, sodass tatsächlich eine echte Liste erforderlich ist. Wenn Sie ihm einen geben, kann er sofort mit der Arbeit beginnen. Wenn Sie ihm stattdessen einen Genexp geben, kann er erst dann mit der Arbeit beginnen, wenn eine neue Liste im Speicher erstellt wurde, indem der Genexp bis zur Erschöpfung ausgeführt wird:

~ $ python -m timeit '"".join(str(n) for n in xrange(1000))'
1000 loops, best of 3: 335 usec per loop
~ $ python -m timeit '"".join([str(n) for n in xrange(1000)])'
1000 loops, best of 3: 288 usec per loop

Das gleiche Ergebnis gilt bei einem Vergleich itertools.imap gegen Karte :

~ $ python -m timeit -s'from itertools import imap' '"".join(imap(str, xrange(1000)))'
1000 loops, best of 3: 220 usec per loop
~ $ python -m timeit '"".join(map(str, xrange(1000)))'
1000 loops, best of 3: 212 usec per loop
Raymond Hettinger
quelle
4
@lazyr Dein zweites Timing macht zu viel Arbeit. Wickeln Sie keinen Genexp um einen Listcomp - verwenden Sie einfach einen Genexp direkt. Kein Wunder, dass du merkwürdige Zeiten hast.
Raymond Hettinger
10
Können Sie erklären, warum ''.join()2 Durchgänge über den Iterator erforderlich sind, um eine Zeichenfolge zu erstellen?
Ovgolovin
26
@ovgolovin Ich denke, der erste Durchgang besteht darin, die Länge der Zeichenfolgen zu summieren, um die richtige Speichermenge für die verkettete Zeichenfolge zuzuweisen, während der zweite Durchgang darin besteht, die einzelnen Zeichenfolgen in den zugewiesenen Speicherplatz zu kopieren.
Lauritz V. Thaulow
19
@lazyr Diese Vermutung ist richtig. Genau das macht str.join :-)
Raymond Hettinger
4
Manchmal vermisse ich wirklich die Fähigkeit, eine bestimmte Antwort auf SO zu "favorisieren".
Air
5

In Ihrem zweiten Beispiel wird eher ein Generatorausdruck als ein Listenverständnis verwendet. Der Unterschied besteht darin, dass mit dem Listenverständnis eine Liste vollständig erstellt und übergeben wird .join(). Mit dem Generatorausdruck werden Elemente einzeln generiert und von verbraucht .join(). Letzteres benötigt weniger Speicher und ist im Allgemeinen schneller.

Zufällig verwendet der Listenkonstruktor gerne alle iterierbaren Elemente, einschließlich eines Generatorausdrucks. So:

[str(n) for n in xrange(10)]

ist nur "syntaktischer Zucker" für:

list(str(n) for n in xrange(10))

Mit anderen Worten, ein Listenverständnis ist nur ein Generatorausdruck, der in eine Liste umgewandelt wird.

irgendwie
quelle
2
Sind Sie sicher, dass sie unter der Haube gleichwertig sind? Timeit sagt :: [str(x) for x in xrange(1000)]262 usec , list(str(x) for x in xrange(1000)): 304 usec.
Lauritz V. Thaulow
2
@lazyr Du hast recht. Das Listenverständnis ist schneller. Und dies ist der Grund, warum Listenverständnisse in Python 2.x auslaufen. Dies ist, was GVR schrieb: "" Dies war ein Artefakt der ursprünglichen Implementierung von Listenverständnissen; Es war jahrelang eines von Pythons "schmutzigen kleinen Geheimnissen". Es begann als absichtlicher Kompromiss, um Listenverständnisse unglaublich schnell zu machen, und obwohl es für Anfänger keine alltägliche Gefahr war,
stach
3
@ovgolovin Der Grund, warum der Listcomp schneller ist, ist, dass Join eine Liste erstellen muss, bevor er mit der Arbeit beginnen kann. Das "Leck", auf das Sie sich beziehen, ist kein Geschwindigkeitsproblem - es bedeutet nur, dass die Schleifeninduktionsvariable außerhalb des Listencomputers verfügbar ist.
Raymond Hettinger
1
@RaymondHettinger Was bedeutet dieses Wort dann "Es begann als absichtlicher Kompromiss, Listenverständnisse blendend schnell zu machen "? Soweit ich verstanden habe, besteht ein Zusammenhang zwischen ihrer Leckage und den Geschwindigkeitsproblemen. GVR schrieb auch: "Für Generatorausdrücke konnten wir dies nicht tun. Generatorausdrücke werden unter Verwendung von Generatoren implementiert, deren Ausführung einen separaten Ausführungsrahmen erfordert. Daher waren Generatorausdrücke (insbesondere wenn sie über eine kurze Sequenz iterieren) weniger effizient als Listenverständnisse . ""
Ovgolovin
3
@ovgolovin Sie haben einen falschen Sprung von einem Listcomp-Implementierungsdetail zu dem Grund gemacht, warum str.join so funktioniert, wie es funktioniert. Eine der ersten Zeilen im str.join-Code ist, seq = PySequence_Fast(orig, "");und dies ist der einzige Grund, warum Iteratoren beim Aufrufen von str.join () langsamer ausgeführt werden als Listen oder Tupel. Sie können gerne einen Chat starten, wenn Sie ihn weiter diskutieren möchten (ich bin der Autor von PEP 289, der Ersteller des Opcodes LIST_APPEND, und derjenige, der den Konstruktor list () optimiert hat, also habe ich einige Vertrautheit mit dem Thema).
Raymond Hettinger
5

Wie bereits erwähnt, handelt es sich um einen Generatorausdruck .

Aus der Dokumentation:

Die Klammern können bei Aufrufen mit nur einem Argument weggelassen werden. Einzelheiten finden Sie im Abschnitt Anrufe .

Mönch
quelle
4

Wenn es in Parens, aber nicht in Klammern steht, ist es technisch gesehen ein Generatorausdruck. Generatorausdrücke wurden erstmals in Python 2.4 eingeführt.

http://wiki.python.org/moin/Generators

Der Teil nach dem Join ( str(_) for _ in xrange(10) )ist für sich genommen ein Generatorausdruck. Sie könnten so etwas tun wie:

mylist = (str(_) for _ in xrange(10))
''.join(mylist)

und es bedeutet genau das gleiche, was Sie im zweiten Fall oben geschrieben haben.

Generatoren haben einige sehr interessante Eigenschaften, nicht zuletzt, dass sie nicht eine ganze Liste zuordnen, wenn Sie keine benötigen. Stattdessen "pumpt" eine Funktion wie "Verbinden" die Elemente einzeln aus dem Generatorausdruck und bearbeitet die winzigen Zwischenteile.

In Ihren speziellen Beispielen arbeiten Liste und Generator wahrscheinlich nicht besonders unterschiedlich, aber im Allgemeinen bevorzuge ich die Verwendung von Generatorausdrücken (und sogar Generatorfunktionen), wann immer ich kann, hauptsächlich, weil es äußerst selten ist, dass ein Generator langsamer als eine vollständige Liste ist Materialisation.

sblom
quelle
1

Das ist eher ein Generator als ein Listenverständnis. Generatoren sind ebenfalls iterabel, aber anstatt zuerst die gesamte Liste zu erstellen und dann an den Join zu übergeben, werden alle Werte im x-Bereich einzeln übergeben, was viel effizienter sein kann.

Daniel Roseman
quelle
0

Das Argument für Ihren zweiten joinAufruf ist ein Generatorausdruck. Es erzeugt eine iterable.

Michael J. Barber
quelle