PEP 289 – Generatorausdrücke
- Autor:
- Raymond Hettinger <python at rcn.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 30. Jan. 2002
- Python-Version:
- 2.4
- Post-History:
- 22. Okt. 2003
Zusammenfassung
Diese PEP führt Generatorausdrücke als eine leistungsstarke, speichereffiziente Verallgemeinerung von Listen-Comprehensions PEP 202 und Generatoren PEP 255 ein.
Begründung
Die Erfahrung mit Listen-Comprehensions hat ihre weit verbreitete Nützlichkeit in ganz Python gezeigt. Viele der Anwendungsfälle müssen jedoch keine vollständige Liste im Speicher erstellen. Stattdessen müssen sie die Elemente nur einzeln durchlaufen.
Zum Beispiel wird der folgende Summationscode eine vollständige Liste von Quadraten im Speicher erstellen, diese Werte durchlaufen und, wenn die Referenz nicht mehr benötigt wird, die Liste löschen.
sum([x*x for x in range(10)])
Der Speicher wird durch die Verwendung eines Generatorausdrucks stattdessen geschont.
sum(x*x for x in range(10))
Ähnliche Vorteile werden den Konstruktoren von Containerobjekten gewährt.
s = set(word for line in page for word in line.split())
d = dict( (k, func(k)) for k in keylist)
Generatorausdrücke sind besonders nützlich mit Funktionen wie sum(), min() und max(), die eine iterierbare Eingabe auf einen einzelnen Wert reduzieren.
max(len(line) for line in file if line.strip())
Generatorausdrücke adressieren auch einige Beispiele von Funktionalen, die mit lambda kodiert wurden.
reduce(lambda s, a: s + a.myattr, data, 0)
reduce(lambda s, a: s + a[3], data, 0)
Diese vereinfachen sich zu
sum(a.myattr for a in data)
sum(a[3] for a in data)
Listen-Comprehensions reduzierten den Bedarf an filter() und map() erheblich. Ebenso wird erwartet, dass Generatorausdrücke den Bedarf an itertools.ifilter() und itertools.imap() minimieren. Im Gegensatz dazu wird die Nützlichkeit anderer itertools durch Generatorausdrücke verbessert.
dotproduct = sum(x*y for x,y in itertools.izip(x_vector, y_vector))
Die ähnliche Syntax wie bei Listen-Comprehensions erleichtert auch die Konvertierung bestehenden Codes in einen Generatorausdruck bei der Skalierung von Anwendungen.
Frühe Zeitmessungen zeigten, dass Generatoren einen signifikanten Leistungsvorteil gegenüber Listen-Comprehensions hatten. Letztere wurden jedoch für Py2.4 stark optimiert und nun ist die Leistung für kleine bis mittelgroße Datensätze ungefähr vergleichbar. Wenn die Datenvolumen größer werden, tendieren Generatorausdrücke zu besseren Leistungen, da sie den Cache-Speicher nicht erschöpfen und Python ermöglicht, Objekte zwischen Iterationen wiederzuverwenden.
BDFL-Bekanntmachungen
Diese PEP ist für Py2.4 ANGENOMMEN.
Die Details
(Nichts davon ist aus der Sicht eines Marsianers exakt genug, aber ich hoffe, die Beispiele vermitteln die Absicht gut genug für eine Diskussion in c.l.py. Das Python-Referenzhandbuch sollte eine 100% exakte semantische und syntaktische Spezifikation enthalten.)
- Die Semantik eines Generatorausdrucks ist äquivalent zur Erstellung und zum Aufruf einer anonymen Generatorfunktion. Zum Beispiel
g = (x**2 for x in range(10)) print g.next()
ist äquivalent zu
def __gen(exp): for x in exp: yield x**2 g = __gen(iter(range(10))) print g.next()
Nur der äußerste for-Ausdruck wird sofort ausgewertet, die anderen Ausdrücke werden verzögert, bis der Generator ausgeführt wird.
g = (tgtexp for var1 in exp1 if exp2 for var2 in exp3 if exp4)
ist äquivalent zu
def __gen(bound_exp): for var1 in bound_exp: if exp2: for var2 in exp3: if exp4: yield tgtexp g = __gen(iter(exp1)) del __gen
- Die Syntax erfordert, dass ein Generatorausdruck immer direkt innerhalb einer Klammer steht und keine Kommas auf beiden Seiten haben darf. Mit Bezug auf die Datei Grammar/Grammar in CVS ändern sich zwei Regeln:
- Die Regel
atom: '(' [testlist] ')'
ändert sich zu
atom: '(' [testlist_gexp] ')'
wobei testlist_gexp fast dasselbe wie listmaker ist, aber nur einen einzelnen Test nach 'for' ... 'in' erlaubt.
testlist_gexp: test ( gen_for | (',' test)* [','] )
- Die Regel für arglist erfordert ähnliche Änderungen.
Das bedeutet, dass Sie schreiben können
sum(x**2 for x in range(10))
aber Sie müssten schreiben
reduce(operator.add, (x**2 for x in range(10)))
und auch
g = (x**2 for x in range(10))
d.h. wenn ein Funktionsaufruf ein einzelnes Positionsargument hat, kann dies ein Generatorausdruck ohne zusätzliche Klammern sein, aber in allen anderen Fällen müssen Sie ihn klammern.
Die genauen Details wurden in Grammar/Grammar Version 1.49 eingecheckt.
- Die Regel
- Die Schleifenvariable (falls es sich um eine einfache Variable oder ein Tupel einfacher Variablen handelt) wird nicht für die umgebende Funktion exponiert. Dies erleichtert die Implementierung und macht typische Anwendungsfälle zuverlässiger. In einer zukünftigen Version von Python werden auch Listen-Comprehensions die Induktionsvariable aus dem umgebenden Code ausblenden (und in Py2.4 werden Warnungen für Code ausgegeben, der auf die Induktionsvariable zugreift).
Zum Beispiel:
x = "hello" y = list(x for x in "abc") print x # prints "hello", not "c"
- Listen-Comprehensions bleiben unverändert. Zum Beispiel
[x for x in S] # This is a list comprehension. [(x for x in S)] # This is a list containing one generator # expression.
Leider gibt es derzeit einen geringfügigen syntaktischen Unterschied. Der Ausdruck
[x for x in 1, 2, 3]
ist legal, was bedeutet
[x for x in (1, 2, 3)]
Aber Generatorausdrücke lassen die erstere Version nicht zu.
(x for x in 1, 2, 3)
ist illegal.
Die frühere Syntax für Listen-Comprehensions wird in Python 3.0 illegal und sollte in Python 2.4 und höher als veraltet markiert werden.
Listen-Comprehensions "leaken" auch ihre Schleifenvariable in den umgebenden Gültigkeitsbereich. Dies wird sich ebenfalls in Python 3.0 ändern, so dass die semantische Definition einer Listen-Comprehension in Python 3.0 äquivalent zu list(<generator expression>) sein wird. Python 2.4 und höher sollten eine Verwarnung ausgeben, wenn die Schleifenvariable einer Listen-Comprehension denselben Namen hat wie eine Variable, die im unmittelbar umgebenden Gültigkeitsbereich verwendet wird.
Frühe Bindung versus späte Bindung
Nach vieler Diskussion wurde entschieden, dass der erste (äußerste) for-Ausdruck sofort ausgewertet werden soll und die restlichen Ausdrücke ausgewertet werden, wenn der Generator ausgeführt wird.
Auf die Frage, die Gründe für die Bindung des ersten Ausdrucks zusammenzufassen, bot Guido [1]
Consider sum(x for x in foo()). Now suppose there's a bug in foo()
that raises an exception, and a bug in sum() that raises an
exception before it starts iterating over its argument. Which
exception would you expect to see? I'd be surprised if the one in
sum() was raised rather the one in foo(), since the call to foo()
is part of the argument to sum(), and I expect arguments to be
processed before the function is called.
OTOH, in sum(bar(x) for x in foo()), where sum() and foo()
are bugfree, but bar() raises an exception, we have no choice but
to delay the call to bar() until sum() starts iterating -- that's
part of the contract of generators. (They do nothing until their
next() method is first called.)
Es wurden verschiedene Anwendungsfälle für die Bindung aller freien Variablen vorgeschlagen, wenn der Generator definiert wird. Und einige Befürworter meinten, dass die resultierenden Ausdrücke leichter zu verstehen und zu debuggen wären, wenn sie sofort gebunden würden.
Python verfolgt jedoch einen späten Bindungsansatz für Lambda-Ausdrücke und hat kein Präzedenzfall für automatische, frühe Bindung. Es wurde als unnötige Komplexität empfunden, ein neues Paradigma einzuführen.
Nachdem viele Möglichkeiten untersucht wurden, bildete sich ein Konsens darüber, dass Bindungsprobleme schwer zu verstehen waren und dass Benutzer dringend ermutigt werden sollten, Generatorausdrücke innerhalb von Funktionen zu verwenden, die ihre Argumente sofort verbrauchen. Für komplexere Anwendungen sind vollständige Generator-Definitionen hinsichtlich Scope, Lebensdauer und Bindung immer überlegen [2].
Reduktionsfunktionen
Die Nützlichkeit von Generatorausdrücken wird erheblich verbessert, wenn sie mit Reduktionsfunktionen wie sum(), min() und max() kombiniert werden. Das heapq-Modul in Python 2.4 enthält zwei neue Reduktionsfunktionen: nlargest() und nsmallest(). Beide funktionieren gut mit Generatorausdrücken und halten zu keinem Zeitpunkt mehr als n Elemente im Speicher.
Danksagungen
- Raymond Hettinger schlug die Idee der "Generator-Comprehensions" erstmals im Januar 2002 vor.
- Peter Norvig belebte die Diskussion in seinem Vorschlag für Accumulation Displays wieder.
- Alex Martelli lieferte kritische Messungen, die die Leistungsvorteile von Generatorausdrücken bewiesen. Er lieferte auch starke Argumente dafür, dass sie wünschenswert seien.
- Phillip Eby schlug "Iterator-Ausdrücke" als Namen vor.
- Anschließend schlug Tim Peters den Namen "Generatorausdrücke" vor.
- Armin Rigo, Tim Peters, Guido van Rossum, Samuele Pedroni, Hye-Shik Chang und Raymond Hettinger erarbeiteten die Probleme rund um frühe versus späte Bindung [1].
- Jiwon Seo implementierte im Alleingang verschiedene Versionen des Vorschlags, einschließlich der endgültigen Version, die in CVS geladen wurde. Auf dem Weg dorthin gab es regelmäßige Code-Reviews von Hye-Shik Chang und Raymond Hettinger. Guido van Rossum traf die wichtigsten Designentscheidungen nach Kommentaren von Armin Rigo und Diskussionen in Newsgroups. Raymond Hettinger lieferte die Testsuite, Dokumentation, Tutorial und Beispiele [2].
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0289.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT