PEP 798 – Unpacking in Comprehensions
- Autor:
- Adam Hartz <hz at mit.edu>, Erik Demaine <edemaine at mit.edu>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Entwurf
- Typ:
- Standards Track
- Erstellt:
- 19-Jul-2025
- Python-Version:
- 3.15
- Post-History:
- 16-Oct-2021, 22-Jun-2025, 19-Jul-2025
Zusammenfassung
Dieser PEP schlägt vor, Listen-, Mengen- und Dictionary-Comprehensions sowie Generator Expressions um die Unpacking-Notation (* und **) am Anfang des Ausdrucks zu erweitern, um eine prägnante Möglichkeit zu bieten, eine beliebige Anzahl von Iterables zu einer einzigen Liste oder Menge oder einem Generator zu kombinieren, oder eine beliebige Anzahl von Dictionaries zu einem einzigen Dictionary, zum Beispiel
[*it for it in its] # list with the concatenation of iterables in 'its'
{*it for it in its} # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its) # generator of the concatenation of iterables in 'its'
Motivation
Die erweiterte Unpacking-Notation (* und **) aus PEP 448 erleichtert das Kombinieren einiger weniger Iterables oder Dictionaries
[*it1, *it2, *it3] # list with the concatenation of three iterables
{*it1, *it2, *it3} # set with the union of three iterables
{**dict1, **dict2, **dict3} # dict with the combination of three dicts
Aber wenn wir eine beliebige Anzahl von Iterables auf ähnliche Weise kombinieren wollen, können wir das Unpacking nicht auf diese Weise verwenden.
Das gesagt, wir haben ein paar Optionen zum Kombinieren mehrerer Iterables. Wir könnten zum Beispiel explizite Schleifenstrukturen und eingebaute Kombinationsmittel verwenden
new_list = []
for it in its:
new_list.extend(it)
new_set = set()
for it in its:
new_set.update(it)
new_dict = {}
for d in dicts:
new_dict.update(d)
def new_generator():
for it in its:
yield from it
Oder wir könnten prägnanter sein, indem wir eine Comprehension mit zwei Schleifen verwenden
[x for it in its for x in it]
{x for it in its for x in it}
{key: value for d in dicts for key, value in d.items()}
(x for it in its for x in it)
Oder wir könnten itertools.chain oder itertools.chain.from_iterable verwenden
list(itertools.chain(*its))
set(itertools.chain(*its))
dict(itertools.chain(*(d.items() for d in dicts)))
itertools.chain(*its)
list(itertools.chain.from_iterable(its))
set(itertools.chain.from_iterable(its))
dict(itertools.chain.from_iterable(d.items() for d in dicts))
itertools.chain.from_iterable(its)
Oder, für alle außer dem Generator, könnten wir functools.reduce verwenden
functools.reduce(operator.iconcat, its, (new_list := []))
functools.reduce(operator.ior, its, (new_set := set()))
functools.reduce(operator.ior, its, (new_dict := {}))
Dieser PEP schlägt vor, Unpacking-Operationen als zusätzliche Alternative in Comprehensions zuzulassen
[*it for it in its] # list with the concatenation of iterables in 'its'
{*it for it in its} # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its) # generator of the concatenation of iterables in 'its'
Dieser Vorschlag erstreckt sich auch auf asynchrone Comprehensions und Generator Expressions, so dass zum Beispiel (*ait async for ait in aits()) äquivalent zu (x async for ait in aits() for x in ait) ist.
Begründung
Das Kombinieren von Iterable-Objekten zu einem einzigen größeren Objekt ist eine häufige Aufgabe. Ein StackOverflow-Post, der sich mit dem Abflachen einer Liste von Listen beschäftigt, wurde beispielsweise 4,6 Millionen Mal angesehen. Obwohl dies eine häufige Operation ist, erfordern die derzeit verfügbaren Optionen zur prägnanten Durchführung ein solches Maß an Indirektion, das den resultierenden Code schwer lesbar und verständlich machen kann.
Die vorgeschlagene Notation ist prägnant (vermeidet die Verwendung und Wiederholung von Hilfsvariablen) und, wie wir erwarten, intuitiv und vertraut für Programmierer, die sowohl mit Comprehensions als auch mit Unpacking-Notation vertraut sind (siehe Codebeispiele für Beispiele aus der Standardbibliothek, die mit der vorgeschlagenen Syntax klarer und prägnanter neu geschrieben werden könnten).
Dieser Vorschlag wurde teilweise durch eine schriftliche Prüfung in einem Python-Programmierkurs motiviert, bei der mehrere Studenten die Notation (insbesondere die set-Version) in ihren Lösungen verwendeten und davon ausgingen, dass sie bereits in Python existiert. Dies deutet darauf hin, dass die Notation auch für Anfänger intuitiv ist. Im Gegensatz dazu ist die vorhandene Syntax [x for it in its for x in it] eine, die Studenten oft falsch machen, da der natürliche Impuls für viele Studenten darin besteht, die Reihenfolge der for-Klauseln umzukehren.
Darüber hinaus zeigt der Kommentarbereich eines Reddit-Posts nach der Veröffentlichung dieses PEP eine beträchtliche Unterstützung für den Vorschlag und deutet ferner darauf hin, dass die hier vorgeschlagene Syntax lesbar, intuitiv und nützlich ist.
Spezifikation
Syntax
Die Grammatik sollte geändert werden, um den Ausdruck in Listen-/Mengen-Comprehensions und Generator Expressions mit einem * präzedieren zu lassen, und um eine alternative Form der Dictionary-Comprehensions zu ermöglichen, bei der ein doppelt gestarner Ausdruck anstelle eines key: value-Paares verwendet werden kann.
Dies kann durch Aktualisierung der listcomp und setcomp Regeln geschehen, um star_named_expression anstelle von named_expression zu verwenden
listcomp[expr_ty]:
| '[' a=star_named_expression b=for_if_clauses ']'
setcomp[expr_ty]:
| '{' a=star_named_expression b=for_if_clauses '}'
Die Regel für genexp müsste ähnlich modifiziert werden, um einen starred_expression zuzulassen
genexp[expr_ty]:
| '(' a=(assignment_expression | expression !':=' | starred_expression) b=for_if_clauses ')'
Die Regel für Dictionary-Comprehensions müsste ebenfalls angepasst werden, um diese neue Form zu ermöglichen
dictcomp[expr_ty]:
| '{' a=double_starred_kvpair b=for_if_clauses '}'
Es sollte keine Änderung an der Art und Weise vorgenommen werden, wie Argument-Unpacking in Funktionsaufrufen gehandhabt wird, d.h. die allgemeine Regel, dass Generator Expressions, die als einziges Argument an Funktionen übergeben werden, keine zusätzlichen redundanten Klammern benötigen, sollte beibehalten werden. Beachten Sie, dass dies impliziert, dass zum Beispiel f(*x for x in it) äquivalent zu f((*x for x in it)) ist (siehe Gestarnt Generatoren als Funktionsargumente für weitere Diskussionen).
* und ** sollten nur auf der obersten Ebene des Ausdrucks in der Comprehension erlaubt sein (siehe Weitere Verallgemeinerung von Unpacking-Operatoren für weitere Diskussionen).
Semantik: Listen-/Mengen-/Dictionary-Comprehensions
Die Bedeutung eines gestarnten Ausdrucks in einer Listen-Comprehension [*expr for x in it] besteht darin, jeden Ausdruck als Iterable zu behandeln und sie zu verketten, auf dieselbe Weise, als wären sie explizit über [*expr1, *expr2, ...] aufgelistet. Ebenso bildet {*expr for x in it} eine Mengenunion, als wären die Ausdrücke explizit über {*expr1, *expr2, ...} aufgelistet; und {**expr for x in it} kombiniert Dictionaries, als wären die Ausdrücke explizit über {**expr1, **expr2, ...} aufgelistet. Diese Operationen sollten alle äquivalenten Semantiken für die Kombination von Sammlungen auf diese Weise beibehalten (einschließlich zum Beispiel, dass spätere Werte frühere bei einem duplizierten Schlüssel beim Kombinieren von Dictionaries ersetzen).
Anders ausgedrückt, die Objekte, die von den folgenden Comprehensions erstellt werden
new_list = [*expr for x in its]
new_set = {*expr for x in its}
new_dict = {**expr for d in dicts}
sollten äquivalent zu den Objekten sein, die von den folgenden Codefragmenten erstellt werden, jeweils
new_list = []
for x in its:
new_list.extend(expr)
new_set = set()
for x in its:
new_set.update(expr)
new_dict = {}
for x in dicts:
new_dict.update(expr)
Semantik: Generator Expressions
Generator Expressions, die die Unpacking-Syntax verwenden, sollten neue Generatoren bilden, die Werte aus der Verkettung der von den Ausdrücken bereitgestellten Iterables erzeugen. Insbesondere ist das Verhalten definiert als äquivalent zum folgenden
# equivalent to g = (*expr for x in it)
def generator():
for x in it:
yield from expr
g = generator()
Da yield from in asynchronen Generatoren nicht erlaubt ist (siehe Abschnitt PEP 525 über asynchrones yield from), ist das Äquivalent für (*expr async for x in ait()) eher wie folgt (obwohl diese neue Form natürlich die Schleifenvariable i nicht definieren oder referenzieren sollte)
# equivalent to g = (*expr async for x in ait())
async def generator():
async for x in ait():
for i in expr:
yield i
g = generator()
Die Details dieser Semantik sollten in Zukunft noch einmal überdacht werden, insbesondere wenn asynchrone Generatoren Unterstützung für yield from erhalten (in diesem Fall könnte die asynchrone Variante geändert werden, um yield from anstelle einer expliziten Schleife zu verwenden). Siehe Alternative Semantik für Generator Expressions für weitere Diskussionen.
Interaktion mit Zuweisungs-Ausdrücken
Beachten Sie, dass dieser Vorschlag keine Änderung der Auswertungsreihenfolge der verschiedenen Teile der Comprehension oder von Regeln bezüglich des Scopes vorschlägt. Dies ist besonders relevant für Generator Expressions, die den "Walross-Operator" := aus PEP 572 verwenden, der, wenn er in einer Comprehension oder Generator Expression verwendet wird, seine Variablendefinition im umgebenden Scope anstatt lokal für die Comprehension durchführt.
Als Beispiel betrachten wir den Generator, der aus der Auswertung des Ausdrucks (*(y := [i, i+1]) for i in (0, 2, 4)) resultiert. Dies ist ungefähr äquivalent zum folgenden Generator, mit der Ausnahme, dass in seiner Generator-Expressionsform y im umgebenden Scope gebunden wird und nicht lokal
def generator():
for i in (0, 2, 4):
yield from (y := [i, i+1])
In diesem Beispiel wird der Teilausdruck (y := [i, i+1]) genau dreimal ausgewertet, bevor der Generator erschöpft ist: unmittelbar nach der Zuweisung von i in der Comprehension zu 0, 2 und 4, jeweils. Daher wird y (im umgebenden Scope) zu diesen Zeitpunkten modifiziert
>>> g = (*(y := [i, i+1]) for i in (0, 2, 4))
>>> y
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
y
NameError: name 'y' is not defined
>>> next(g)
0
>>> y
[0, 1]
>>> next(g)
1
>>> y
[0, 1]
>>> next(g)
2
>>> y
[2, 3]
Fehlermeldung
Derzeit erzeugt die vorgeschlagene Syntax einen SyntaxError. Damit diese Formen als syntaktisch gültig anerkannt werden, müssen die Grammatikregeln für invalid_comprehension und invalid_dict_comprehension angepasst werden, um die Verwendung von * bzw. ** zuzulassen.
Zusätzliche spezifische Fehlermeldungen sollten zumindest in den folgenden Fällen bereitgestellt werden
- Der Versuch,
**in einer Listen-Comprehension oder Generator Expression zu verwenden, sollte melden, dass Dictionary-Unpacking in diesen Strukturen nicht verwendet werden kann, zum Beispiel>>> [**x for x in y] File "<stdin>", line 1 [**x for x in y] ^^^ SyntaxError: cannot use dict unpacking in list comprehension >>> (**x for x in y) File "<stdin>", line 1 (**x for x in y) ^^^ SyntaxError: cannot use dict unpacking in generator expression
- Die bestehende Fehlermeldung für den Versuch,
*bei einem Dictionary-Schlüssel/Wert zu verwenden, sollte beibehalten werden, aber ähnliche Meldungen sollten ausgegeben werden, wenn versucht wird,**-Unpacking auf einen Dictionary-Schlüssel oder -Wert anzuwenden, zum Beispiel>>> {*k: v for k,v in items} File "<stdin>", line 1 {*k: v for k,v in items} ^^ SyntaxError: cannot use a starred expression in a dictionary key >>> {k: *v for k,v in items} File "<stdin>", line 1 {k: *v for k,v in items} ^^ SyntaxError: cannot use a starred expression in a dictionary value >>> {**k: v for k,v in items} File "<stdin>", line 1 {**k: v for k,v in items} ^^^ SyntaxError: cannot use dict unpacking in a dictionary key >>> {k: **v for k,v in items} File "<stdin>", line 1 {k: **v for k,v in items} ^^^ SyntaxError: cannot use dict unpacking in a dictionary value
- Die Formulierung einiger anderer bestehender Fehlermeldungen sollte ebenfalls angepasst werden, um die neue Syntax zu berücksichtigen und/oder unklare oder verwirrende Fälle im Zusammenhang mit Unpacking im Allgemeinen zu klären (insbesondere die Fälle, die in Weitere Verallgemeinerung von Unpacking-Operatoren erwähnt werden), zum Beispiel
>>> [*x if x else y] File "<stdin>", line 1 [*x if x else y] ^^^^^^^^^^^^^^ SyntaxError: invalid starred expression. Did you forget to wrap the conditional expression in parentheses? >>> {**x if x else y} File "<stdin>", line 1 {**x if x else y} ^^^^^^^^^^^^^^^ SyntaxError: invalid double starred expression. Did you forget to wrap the conditional expression in parentheses? >>> [x if x else *y] File "<stdin>", line 1 [x if x else *y] ^ SyntaxError: cannot unpack only part of a conditional expression >>> {x if x else **y} File "<stdin>", line 1 {x if x else **y} ^^ SyntaxError: cannot use dict unpacking on only part of a conditional expression
Referenzimplementierung
Die Referenzimplementierung implementiert diese Funktionalität, einschließlich Entwurfsdokumentation und zusätzlicher Testfälle.
Abwärtskompatibilität
Das Verhalten aller derzeit syntaktisch gültigen Comprehensions würde durch diese Änderung nicht beeinträchtigt, daher erwarten wir keine größeren Kompatibilitätsprobleme. Grundsätzlich würde diese Änderung nur Code betreffen, der sich darauf verlässt, dass der Versuch, Unpacking-Operationen in Comprehensions zu verwenden, einen SyntaxError auslöst, oder der sich auf die spezifische Formulierung von alten Fehlermeldungen verlässt, die ersetzt werden, was wir für selten halten.
Eine damit zusammenhängende Sorge ist, dass eine hypothetische zukünftige Entscheidung, die Semantik von asynchronen Generator Expressions zu ändern, um yield from beim Unpacking zu verwenden (Delegation an Generatoren, die entpackt werden), nicht abwärtskompatibel wäre, da sie das Verhalten der resultierenden Generatoren bei Verwendung mit .asend(), .athrow() und .aclose() beeinflussen würde. Trotzdem wäre eine solche Änderung, obwohl abwärtsinkompatibel, unwahrscheinlich, eine große Auswirkung zu haben, da sie nur das Verhalten von Strukturen betreffen würde, die unter diesem Vorschlag nicht besonders nützlich sind. Siehe Alternative Semantik für Generator Expressions für weitere Diskussionen.
Codebeispiele
Dieser Abschnitt zeigt einige illustrative Beispiele dafür, wie kleine Codefragmente aus der Standardbibliothek umgeschrieben werden könnten, um diese neue Syntax zu nutzen und die Prägnanz und Lesbarkeit zu verbessern. Die Referenzimplementierung besteht alle Tests mit diesen Ersetzungen fort.
Ersetzen expliziter Schleifen
Das Ersetzen expliziter Schleifen komprimiert mehrere Zeilen auf eine und vermeidet die Notwendigkeit, eine Hilfsvariable zu definieren und zu referenzieren.
- Aus
email/_header_value_parser.py# current: comments = [] for token in self: comments.extend(token.comments) return comments # improved: return [*token.comments for token in self]
- Aus
shutil.py# current: ignored_names = [] for pattern in patterns: ignored_names.extend(fnmatch.filter(names, pattern)) return set(ignored_names) # improved: return {*fnmatch.filter(names, pattern) for pattern in patterns}
- Aus
http/cookiejar.py# current: cookies = [] for domain in self._cookies.keys(): cookies.extend(self._cookies_for_domain(domain, request)) return cookies # improved: return [ *self._cookies_for_domain(domain, request) for domain in self._cookies.keys() ]
Ersetzen von `from_iterable` und Freunden
Obwohl nicht immer die richtige Wahl, kann das Ersetzen von itertools.chain.from_iterable und map eine zusätzliche Ebene der Umleitung vermeiden, was zu Code führt, der der konventionellen Weisheit folgt, dass Comprehensions besser lesbar sind als map/filter.
- Aus
dataclasses.py# current: inherited_slots = set( itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])) ) # improved: inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}
- Aus
importlib/metadata/__init__.py# current: return itertools.chain.from_iterable( path.search(prepared) for path in map(FastPath, paths) ) # improved: return (*FastPath(path).search(prepared) for path in paths)
- Aus
collections/__init__.py(KlasseCounter)# current: return _chain.from_iterable(_starmap(_repeat, self.items())) # improved: return (*_repeat(elt, num) for elt, num in self.items())
- Aus
zipfile/_path/__init__.py# current: parents = itertools.chain.from_iterable(map(_parents, names)) # improved: parents = (*_parents(name) for name in names)
- Aus
_pyrepl/_module_completer.py# current: search_locations = set(chain.from_iterable( getattr(spec, 'submodule_search_locations', []) for spec in specs if spec )) # improved: search_locations = { *getattr(spec, 'submodule_search_locations', []) for spec in specs if spec }
Ersetzen von doppelten Schleifen in Comprehensions
Das Ersetzen von doppelten Schleifen in Comprehensions vermeidet die Notwendigkeit, eine Hilfsvariable zu definieren und zu referenzieren, und reduziert Unordnung.
- Aus
importlib/resources/readers.py# current: children = (child for path in self._paths for child in path.iterdir()) # improved: children = (*path.iterdir() for path in self._paths)
- Aus
asyncio/base_events.py# current: exceptions = [exc for sub in exceptions for exc in sub] # improved: exceptions = [*sub for sub in exceptions]
- Aus
_weakrefset.py# current: return self.__class__(e for s in (self, other) for e in s) # improved: return self.__class__(*s for s in (self, other))
Wie man das lehrt
Derzeit ist eine übliche Methode, die Vorstellung von Comprehensions einzuführen (die vom Python-Tutorial verwendet wird), äquivalenten Code zu demonstrieren. Zum Beispiel würde diese Methode sagen, dass zum Beispiel out = [expr for x in it] dem folgenden Code äquivalent ist
out = []
for x in it:
out.append(expr)
Mit diesem Ansatz können wir out = [*expr for x in it] als stattdessen äquivalent zu folgendem einführen (das extend anstelle von append verwendet)
out = []
for x in it:
out.extend(expr)
Mengen- und Dictionary-Comprehensions, die Unpacking verwenden, können ebenfalls durch eine ähnliche Analogie eingeführt werden
# equivalent to out = {expr for x in it}
out = set()
for x in it:
out.add(expr)
# equivalent to out = {*expr for x in it}
out = set()
for x in it:
out.update(expr)
# equivalent to out = {k_expr: v_expr for x in it}
out = {}
for x in it:
out[k_expr] = v_expr
# equivalent to out = {**expr for x in it}, provided that expr evaluates to
# a mapping that can be unpacked with **
out = {}
for x in it:
out.update(expr)
Und wir können einen ähnlichen Ansatz verwenden, um das Verhalten von Generator Expressions zu veranschaulichen, die Unpacking beinhalten
# equivalent to g = (expr for x in it)
def generator():
for x in it:
yield expr
g = generator()
# equivalent to g = (*expr for x in it)
def generator():
for x in it:
yield from expr
g = generator()
Wir können dann von diesen spezifischen Beispielen zu der Idee verallgemeinern, dass dort, wo eine nicht-gestarnten Comprehension/Genexp einen Operator verwenden würde, der ein einzelnes Element zu einer Sammlung hinzufügt, die gestarnten stattdessen einen Operator verwenden würden, der mehrere Elemente zu dieser Sammlung hinzufügt.
Alternativ müssen wir die beiden Ideen nicht als getrennt betrachten; stattdessen können wir mit der neuen Syntax out = [...x... for x in it] als äquivalent zum Folgenden betrachten [1] (wobei ...x... ein Platzhalter für beliebigen Code ist), unabhängig davon, ob ...x... * verwendet oder nicht
out = []
for x in it:
out.extend([...x...])
Ebenso können wir out = {...x... for x in it} als äquivalent zum folgenden Code betrachten, unabhängig davon, ob ...x... * oder ** oder : verwendet
out = set() # or out = {}
for x in it:
out.update({...x...})
Diese Beispiele sind in dem Sinne äquivalent, dass die von ihnen erzeugte Ausgabe sowohl in der Version mit der Comprehension als auch in der Version ohne sie gleich wäre, aber beachten Sie, dass die Nicht-Comprehension-Version etwas weniger effizient ist, da sie neue Listen/Mengen/Dictionaries vor jeder extend oder update erstellt, was in der Version, die Comprehensions verwendet, unnötig ist.
Abgelehnte alternative Vorschläge
Das Hauptziel bei der Ausarbeitung der obigen Spezifikation war die Konsistenz mit den bestehenden Normen rund um Unpacking und Comprehensions / Generator Expressions. Eine Interpretation davon ist, dass das Ziel darin bestand, die Spezifikation so zu schreiben, dass die kleinstmöglichen Änderungen an der bestehenden Grammatik und Codeerzeugung erforderlich sind, und den bestehenden Code die umgebende Semantik informieren zu lassen.
Im Folgenden werden einige der häufigen Bedenken/alternativen Vorschläge diskutiert, die in den Diskussionen aufkamen, aber nicht in diesem Vorschlag enthalten sind.
Gestarnt Generatoren als Funktionsargumente
Ein häufiges Bedenken, das mehrmals aufkam (nicht nur in den oben verlinkten Diskussionsfäden, sondern auch in früheren Diskussionen über diese gleiche Idee), ist eine mögliche syntaktische Mehrdeutigkeit beim Übergeben eines gestarnten Generators als einziges Argument an f(*x for x in y). Im ursprünglichen PEP 448 wurde diese Mehrdeutigkeit als Grund dafür angeführt, keine ähnliche Verallgemeinerung als Teil des Vorschlags aufzunehmen.
Dieser Vorschlag legt nahe, dass f(*x for x in y) als f((*x for x in y)) interpretiert werden sollte und kein weiteres Unpacking des resultierenden Generators versuchen sollte, aber mehrere Alternativen wurden in unserer Diskussion vorgeschlagen (und/oder wurden in der Vergangenheit vorgeschlagen), darunter
- Interpretation von
f(*x for x in y)alsf(*(x for x in y), - Interpretation von
f(*x for x in y)alsf(*(*x for x in y)), oder - weiterhin einen
SyntaxErrorfürf(*x for x in y)auszulösen, auch wenn die anderen Aspekte dieses Vorschlags akzeptiert werden.
Der Grund, diesen Vorschlag gegenüber diesen Alternativen zu bevorzugen, ist die Beibehaltung bestehender Konventionen für die Satzzeichen um Generator Expressions. Derzeit ist die allgemeine Regel, dass Generator Expressions in Klammern gesetzt werden müssen, es sei denn, sie werden als einziges Argument an eine Funktion übergeben, und dieser Vorschlag schlägt vor, diese Regel auch dann beizubehalten, wenn wir mehr Arten von Generator Expressions zulassen. Diese Option erhält eine vollständige Symmetrie zwischen Comprehensions und Generator Expressions, die Unpacking verwenden, und denen, die es nicht tun.
Derzeit haben wir die folgenden Konventionen
f([x for x in y]) # pass in a single list
f({x for x in y}) # pass in a single set
f(x for x in y) # pass in a single generator (no additional parentheses required around genexp)
f(*[x for x in y]) # pass in elements from the list separately
f(*{x for x in y}) # pass in elements from the set separately
f(*(x for x in y)) # pass in elements from the generator separately (parentheses required)
Dieser Vorschlag optiert dafür, diese Konventionen beizubehalten, auch wenn die Comprehensions Unpacking verwenden
f([*x for x in y]) # pass in a single list
f({*x for x in y}) # pass in a single set
f(*x for x in y) # pass in a single generator (no additional parentheses required around genexp)
f(*[*x for x in y]) # pass in elements from the list separately
f(*{*x for x in y}) # pass in elements from the set separately
f(*(*x for x in y)) # pass in elements from the generator separately (parentheses required)
Weitere Verallgemeinerung von Unpacking-Operatoren
Ein weiterer Vorschlag, der aus der Diskussion hervorgegangen ist, betraf die weitere Verallgemeinerung von * über die bloße Zulassung der Verwendung zum Unpacking des Ausdrucks in einer Comprehension hinaus. Zwei Hauptvarianten dieser Erweiterung wurden in Betracht gezogen
- machen
*und**zu echten unären Operatoren, die eine neue Art vonUnpackable-Objekt (oder ähnlich) erstellen, das Comprehensions durch Unpacking behandeln könnten, das aber auch in anderen Kontexten verwendet werden könnte; oder - weiterhin
*und**nur an den Stellen zuzulassen, an denen sie an anderer Stelle in diesem Vorschlag erlaubt sind (Ausdruckslisten, Comprehensions, Generator Expressions und Argumentlisten), aber sie auch in Teilausdrücken innerhalb einer Comprehension zu verwenden, was zum Beispiel Folgendes als Möglichkeit zur Abflachung einer Liste ermöglicht, die einige Iterables, aber einige nicht-iterierbare Objekte enthält[*x if isinstance(x, Iterable) else x for x in [[1,2,3], 4]]
Diese Varianten wurden als erheblich komplexer (sowohl im Verständnis als auch in der Implementierung) und von nur marginalem Nutzen erachtet, so dass keine davon in diesem PEP enthalten ist. Daher sollten diese Formen weiterhin einen SyntaxError auslösen, jedoch mit einer neuen Fehlermeldung wie oben beschrieben, obwohl dies nicht als zukünftiger Vorschlag ausgeschlossen werden sollte.
Alternative Semantik für Generator Expressions
Ein weiterer Diskussionspunkt drehte sich um die Semantik des Unpackings in Generator Expressions, insbesondere die Beziehung zwischen der Semantik von synchronen und asynchronen Generator Expressions, da asynchrone Generatoren yield from nicht unterstützen (siehe Abschnitt PEP 525 über asynchrone yield from).
Die Kernfrage drehte sich darum, ob synchrone und asynchrone Generator Expressions yield from (oder ein Äquivalent) beim Unpacking verwenden sollten, im Gegensatz zu einer expliziten Schleife. Der Hauptunterschied zwischen diesen Optionen ist, ob der resultierende Generator an die entpackten Objekte delegiert, was das Verhalten dieser Generator Expressions bei Verwendung mit .send()/.asend(), .throw()/.athrow() und .close()/.aclose() beeinflussen würde, falls die entpackten Objekte selbst Generatoren sind. Die Unterschiede zwischen diesen Optionen sind in Anhang: Semantik der Generator-Delegation zusammengefasst.
Mehrere vernünftige Optionen wurden in Betracht gezogen, von denen keine eine klare Siegerin in einer Umfrage im Discourse-Thread war. Abgesehen von dem oben skizzierten Vorschlag wurden auch die folgenden in Betracht gezogen
- Verwendung expliziter Schleifen sowohl für synchrone als auch für asynchrone Generator Expressions.
Diese Strategie hätte zu einer Symmetrie zwischen synchronen und asynchronen Generator Expressions geführt, aber ein potenziell nützliches Werkzeug verhindert, indem die Delegation im Fall von synchronen Generator Expressions nicht zugelassen wird. Ein spezifisches Bedenken bei diesem Ansatz ist die Einführung einer Asymmetrie zwischen synchronen und asynchronen Generatoren, aber dieses Bedenken wird gemildert durch die Tatsache, dass diese Asymmetrien bereits zwischen synchronen und asynchronen Generatoren im Allgemeinen bestehen.
- Verwendung von
yield fromzum Unpacking in synchronen Generator Expressions und Nachahmung des Verhaltens vonyield fromzum Unpacking in asynchronen Generator Expressions.Diese Strategie würde auch das Unpacking in synchronen und asynchronen Generatoren symmetrisch verhalten lassen, aber sie wäre auch komplexer, genug, dass der Aufwand den Nutzen möglicherweise nicht wert ist. Daher schlägt dieser PEP vor, dass Generator Expressions, die den Unpacking-Operator verwenden, keine Semantik ähnlich
yield fromverwenden sollten, bisyield fromgenerell in asynchronen Generatoren unterstützt wird. - Verwendung von
yield fromzum Unpacking in synchronen Generator Expressions und Verbot des Unpackings in asynchronen Generator Expressions, bis dieseyield fromunterstützen.Diese Strategie könnte möglicherweise die Reibung verringern, wenn asynchrone Generator Expressions die Unterstützung für
yield fromin Zukunft erhalten, indem sichergestellt wird, dass jede Entscheidung, die dann getroffen wird, vollständig abwärtskompatibel ist. Aber der Nutzen des Unpackings in diesem Kontext scheint den potenziellen Nachteil einer minimal-invasiven abwärtsinkompatiblen Änderung in der Zukunft zu überwiegen, falls asynchrone Generator Expressions Unterstützung füryield fromerhalten. - Verbot des Unpackings in allen Generator Expressions.
Dies würde die Symmetrie zwischen beiden Fällen beibehalten, jedoch mit dem Nachteil des Verlusts einer sehr ausdrucksstarken Form.
Jede dieser Optionen (einschließlich der in diesem PEP vorgestellten) hat ihre Vor- und Nachteile, wobei keine Option in allen Punkten klar überlegen ist. Die in Semantik: Generator Expressions vorgeschlagene Semantik stellt einen vernünftigen Kompromiss dar, bei dem das Unpacking in sowohl synchronen als auch asynchronen Generator Expressions die gängigen Wege widerspiegelt, wie äquivalente Generatoren derzeit geschrieben werden. Darüber hinaus sind diese subtilen Unterschiede unwahrscheinlich, Auswirkungen auf gängige Anwendungsfälle zu haben (zum Beispiel gibt es keinen Unterschied für den wahrscheinlich häufigsten Anwendungsfall, einfache Sammlungen zu kombinieren).
Wie oben vorgeschlagen, sollte diese Entscheidung im Falle einer zukünftigen Unterstützung von asynchronen Generatoren für yield from überdacht werden. In diesem Fall sollte die Anpassung der Semantik des Unpackings in asynchronen Generator Expressions zur Verwendung von yield from in Betracht gezogen werden.
Bedenken und Nachteile
Obwohl der allgemeine Konsens aus dem Diskussionsfaden zu sein schien, dass diese Syntax klar und intuitiv war, wurden auch mehrere Bedenken und potenzielle Nachteile geäußert. Dieser Abschnitt fasst diese Bedenken zusammen.
- Überschneidung mit bestehenden Alternativen: Während die vorgeschlagene Syntax arguably klarer und prägnanter ist, gibt es bereits mehrere Möglichkeiten, dasselbe zu erreichen.
- Mehrdeutigkeit bei Funktionsaufrufen: Ausdrücke wie
f(*x for x in y)können zunächst mehrdeutig erscheinen, da nicht sofort ersichtlich ist, ob die Absicht ist, den Generator zu entpacken oder ihn als einzelnes Argument zu übergeben. Obwohl dieser Vorschlag bestehende Konventionen beibehält, indem er diese Form als äquivalent zuf((*x for x in y))behandelt, mag diese Äquivalenz nicht sofort ersichtlich sein. - Potenzial für Übernutzung oder Missbrauch: Komplexe Verwendungen von Unpacking in Comprehensions können eine Logik verschleiern, die in einer expliziten Schleife klarer wäre. Während dies bereits eine allgemeine Sorge bei Comprehensions ist, kann die Hinzufügung von
*und**besonders komplexe Verwendungen noch schwieriger auf einen Blick lesbar und verständlich machen. Zum Beispiel, obwohl diese Situationen wahrscheinlich selten sind, können Comprehensions, die Unpacking auf vielfältige Weise verwenden, es schwierig machen zu wissen, was entpackt wird und wann:f(*(*x for *x, _ in list_of_lists)). - Unklare Beschränkung des Geltungsbereichs: Dieser Vorschlag beschränkt das Unpacking auf die oberste Ebene des Comprehension-Ausdrucks, aber einige Benutzer könnten erwarten, dass der Unpacking-Operator weiter verallgemeinert wird, wie in Weitere Verallgemeinerung von Unpacking-Operatoren diskutiert.
- Auswirkungen auf externe Werkzeuge: Wie bei jeder Änderung der Python-Syntax würde diese Änderung Arbeit für die Wartung von Code-Formatierern, Lintern, Typ-Checkern usw. bedeuten, um sicherzustellen, dass die neue Syntax unterstützt wird.
Anhang: Andere Sprachen
Ziemlich viele andere Sprachen unterstützen diese Art von Abflachung mit einer Syntax, die der in Python bereits verfügbaren ähnelt, aber die Unterstützung für die Verwendung von Unpacking-Syntax innerhalb von Comprehensions ist selten. Dieser Abschnitt bietet eine kurze Zusammenfassung der Unterstützung für ähnliche Syntax in einigen anderen Sprachen.
Viele Sprachen, die Comprehensions unterstützen, unterstützen doppelte Schleifen
# python
[x for xs in [[1,2,3], [], [4,5]] for x in xs * 2]
-- haskell
[x | xs <- [[1,2,3], [], [4,5]], x <- xs ++ xs]
# julia
[x for xs in [[1,2,3], [], [4,5]] for x in [xs; xs]]
; clojure
(for [xs [[1 2 3] [] [4 5]] x (concat xs xs)] x)
Mehrere andere Sprachen (auch solche ohne Comprehensions) unterstützen diese Operationen über eine eingebaute Funktion oder Methode zur Unterstützung der Abflachung von verschachtelten Strukturen
# python
list(itertools.chain(*(xs*2 for xs in [[1,2,3], [], [4,5]])))
// javascript
[[1,2,3], [], [4,5]].flatMap(xs => [...xs, ...xs])
-- haskell
concat (map (\x -> x ++ x) [[1,2,3], [], [4,5]])
# ruby
[[1, 2, 3], [], [4, 5]].flat_map {|e| e * 2}
Sprachen, die sowohl Comprehensions als auch Unpacking unterstützen, erlauben jedoch tendenziell kein Unpacking innerhalb einer Comprehension. Zum Beispiel führt der folgende Ausdruck in Julia derzeit zu einem Syntaxfehler
[xs... for xs in [[1,2,3], [], [4,5]]]
Als Gegenbeispiel wurde kürzlich eine ähnliche Syntax in Civet hinzugefügt. Zum Beispiel ist Folgendes eine gültige Comprehension in Civet, die die ...-Syntax von JavaScript für Unpacking verwendet
for xs of [[1,2,3], [], [4,5]] then ...(xs++xs)
Anhang: Semantik der Generator-Delegation
Eine der häufigen Fragen zur oben beschriebenen Semantik betraf den Unterschied zwischen der Verwendung von yield from beim Entpacken innerhalb eines Generatorausdrucks und der Verwendung einer expliziten Schleife. Da dies ein recht fortgeschrittenes Merkmal von Generatoren ist, versucht dieser Anhang, einige der Hauptunterschiede zwischen Generatoren, die yield from verwenden, und solchen, die explizite Schleifen verwenden, zusammenzufassen.
Grundlegendes Verhalten
Für einfache Iterationen über Werte, was voraussichtlich die mit Abstand häufigste Verwendung von Entpackungen in Generatorausdrücken sein wird, liefern beide Ansätze identische Ergebnisse.
def yield_from(iterables):
for iterable in iterables:
yield from iterable
def explicit_loop(iterables):
for iterable in iterables:
for item in iterable:
yield item
# Both produce the same sequence of values
x = list(yield_from([[1, 2], [3, 4]]))
y = list(explicit_loop([[1, 2], [3, 4]]))
print(x == y) # prints True
Unterschiede im fortgeschrittenen Generator-Protokoll
Die Unterschiede werden deutlich, wenn die fortgeschrittenen Generatorprotokollmethoden .send(), .throw() und .close() verwendet werden und wenn die Unter-Iterierbaren selbst Generatoren anstelle einfacher Sequenzen sind. In diesen Fällen führt die yield from-Version dazu, dass das zugehörige Signal den Subgenerator erreicht, die Version mit der expliziten Schleife jedoch nicht.
Delegation mit .send()
def sub_generator():
x = yield "first"
yield f"received: {x}"
yield "last"
def yield_from():
yield from sub_generator()
def explicit_loop():
for item in sub_generator():
yield item
# With yield from, values are passed through to sub-generator
gen1 = yield_from()
print(next(gen1)) # prints "first"
print(gen1.send("hello")) # prints "received: hello"
print(next(gen1)) # prints "last"
# With explicit loop, .send() affects the outer generator; values don't reach the sub-generator
gen2 = explicit_loop()
print(next(gen2)) # prints "first"
print(gen2.send("hello")) # prints "received: None" (sub-generator receives None instead of "hello")
print(next(gen2)) # prints "last"
Fehlerbehandlung mit .throw()
def sub_generator_with_exception_handling():
try:
yield "first"
yield "second"
except ValueError as e:
yield f"caught: {e}"
def yield_from():
yield from sub_generator_with_exception_handling()
def explicit_loop():
for item in sub_generator_with_exception_handling():
yield item
# With yield from, exceptions are passed to sub-generator
gen1 = yield_from()
print(next(gen1)) # prints "first"
print(gen1.throw(ValueError("test"))) # prints "caught: test"
# With explicit loop, exceptions affect the outer generator only
gen2 = explicit_loop()
print(next(gen2)) # prints "first"
print(gen2.throw(ValueError("test"))) # ValueError is raised; sub-generator doesn't see it
Generator-Bereinigung mit .close()
# hold references to sub-generators so GC doesn't close the explicit loop version
references = []
def sub_generator_with_cleanup():
try:
yield "first"
yield "second"
finally:
print("sub-generator received GeneratorExit")
def yield_from():
try:
g = sub_generator_with_cleanup()
references.append(g)
yield from g
finally:
print("outer generator received GeneratorExit")
def explicit_loop():
try:
g = sub_generator_with_cleanup()
references.append(g)
for item in g:
yield item
finally:
print("outer generator received GeneratorExit")
# With yield from, GeneratorExit is passed through to sub-generator
gen1 = yield_from()
print(next(gen1)) # prints "first"
gen1.close() # closes sub-generator and then outer generator
# With explicit loop, GeneratorExit goes to outer generator only
gen2 = explicit_loop()
print(next(gen2)) # prints "first"
gen2.close() # only closes outer generator
print('program finished; GC will close the explicit loop subgenerator')
# second inner generator closes when GC closes it at the end
Referenzen
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0798.rst
Zuletzt geändert: 2025-09-15 17:40:01 GMT