PEP 572 – Zuweisungsausdrücke
- Autor:
- Chris Angelico <rosuav at gmail.com>, Tim Peters <tim.peters at gmail.com>, Guido van Rossum <guido at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 28-Feb-2018
- Python-Version:
- 3.8
- Post-History:
- 28-Feb-2018, 02-Mär-2018, 23-Mär-2018, 04-Apr-2018, 17-Apr-2018, 25-Apr-2018, 09-Jul-2018, 05-Aug-2019
- Resolution:
- Python-Dev Nachricht
Inhaltsverzeichnis
- Zusammenfassung
- Begründung
- Syntax und Semantik
- Änderungen der Spezifikation während der Implementierung
- Beispiele
- Abgelehnte alternative Vorschläge
- Häufig gestellte Einwände
- Stilrichtlinienempfehlungen
- Danksagungen
- Anhang A: Erkenntnisse von Tim Peters
- Anhang B: Grobe Codeübersetzungen für Abstraktionen
- Anhang C: Keine Änderungen an den Gültigkeitsbereichssemantiken
- Referenzen
- Urheberrecht
Zusammenfassung
Dies ist ein Vorschlag zur Schaffung einer Möglichkeit, Variablen innerhalb eines Ausdrucks mit der Notation NAME := expr zuzuweisen.
Als Teil dieser Änderung gibt es auch eine Aktualisierung der Auswertungsreihenfolge von Dictionary-Abstraktionen, um sicherzustellen, dass Schlüssel-Ausdrücke vor Wert-Ausdrücken ausgeführt werden (wodurch der Schlüssel an einen Namen gebunden und dann als Teil der Berechnung des entsprechenden Werts wiederverwendet werden kann).
Während der Diskussion dieses PEP wurde der Operator informell als „Walross-Operator“ bekannt. Der formelle Name des Konstrukts ist „Assignment Expressions“ (gemäß dem Titel des PEP), sie können aber auch als „Named Expressions“ bezeichnet werden (z.B. verwendet die CPython-Referenzimplementierung diesen Namen intern).
Begründung
Das Benennen des Ergebnisses eines Ausdrucks ist ein wichtiger Teil der Programmierung, der es ermöglicht, einen beschreibenden Namen anstelle eines längeren Ausdrucks zu verwenden und Wiederverwendung zu ermöglichen. Derzeit ist diese Funktion nur in Form einer Anweisung verfügbar, wodurch sie in Listen-Abstraktionen und anderen Ausdruckskontexten nicht verfügbar ist.
Zusätzlich kann das Benennen von Teilbereichen eines großen Ausdrucks einem interaktiven Debugger helfen, nützliche Anzeige-Hooks und Teilergebnisse bereitzustellen. Ohne eine Möglichkeit, Teilausdrücke inline zu erfassen, wäre dies eine Refaktorierung des ursprünglichen Codes erforderlich; mit Zuweisungsausdrücken erfordert dies lediglich die Einfügung einiger name := Marker. Die Notwendigkeit der Refaktorierung zu beseitigen, verringert die Wahrscheinlichkeit, dass der Code als Teil des Debuggens unabsichtlich geändert wird (eine häufige Ursache für Heisenbugs) und ist leichter einem anderen Programmierer zu diktieren.
Die Bedeutung von echtem Code
Während der Entwicklung dieses PEP hatten viele Leute (sowohl Befürworter als auch Kritiker) die Tendenz, sich einerseits auf Spielzeugbeispiele und andererseits auf übermäßig komplexe Beispiele zu konzentrieren.
Die Gefahr von Spielzeugbeispielen ist zweifach: Sie sind oft zu abstrakt, um jemanden zu einem „Wow, das ist überzeugend“ zu bewegen, und sie werden leicht mit „Das würde ich sowieso nie so schreiben“ widerlegt.
Die Gefahr von übermäßig komplexen Beispielen besteht darin, dass sie einem Kritiker des Vorschlags ein bequemes Strohmann-Argument liefern, das er zerpflücken kann („Das ist obskur“).
Dennoch gibt es einen gewissen Nutzen sowohl für extrem einfache als auch für extrem komplexe Beispiele: Sie sind hilfreich, um die beabsichtigte Semantik zu klären. Daher wird unten von beidem etwas geben.
Um jedoch *überzeugend* zu sein, sollten Beispiele in echtem Code verwurzelt sein, d.h. in Code, der ohne Gedanken an dieses PEP als Teil einer nützlichen Anwendung geschrieben wurde, egal wie groß oder klein. Tim Peters war äußerst hilfreich, indem er sein eigenes persönliches Code-Repository durchsah und Beispiele von Code auswählte, den er geschrieben hatte und der (seiner Meinung nach) klarer gewesen wäre, wenn er mit (sparsamer) Verwendung von Zuweisungsausdrücken neu geschrieben worden wäre. Seine Schlussfolgerung: Der aktuelle Vorschlag hätte in einigen Code-Teilen eine bescheidene, aber klare Verbesserung ermöglicht.
Ein weiterer Nutzen von echtem Code ist die indirekte Beobachtung, wie viel Wert Programmierer auf Kompaktheit legen. Guido van Rossum durchsuchte eine Dropbox-Codebasis und entdeckte einige Hinweise darauf, dass Programmierer es vorziehen, weniger Zeilen zu schreiben als kürzere Zeilen.
Ein Beispiel dafür: Guido fand mehrere Beispiele, bei denen ein Programmierer einen Teilausdruck wiederholte, was das Programm verlangsamte, um eine Codezeile zu sparen, z.B. anstatt zu schreiben
match = re.match(data)
group = match.group(1) if match else None
sie würden schreiben
group = re.match(data).group(1) if re.match(data) else None
Ein weiteres Beispiel zeigt, dass Programmierer manchmal mehr Arbeit leisten, um eine zusätzliche Einrückungsebene zu sparen
match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
result = match1.group(1)
elif match2:
result = match2.group(2)
else:
result = None
Dieser Code versucht, pattern2 abzugleichen, auch wenn pattern1 einen Treffer hat (in diesem Fall wird der Treffer auf pattern2 nie verwendet). Die effizientere Überarbeitung wäre gewesen
match1 = pattern1.match(data)
if match1:
result = match1.group(1)
else:
match2 = pattern2.match(data)
if match2:
result = match2.group(2)
else:
result = None
Syntax und Semantik
In den meisten Kontexten, in denen beliebige Python-Ausdrücke verwendet werden können, kann ein **benannter Ausdruck** erscheinen. Dies hat die Form NAME := expr, wobei expr ein beliebiger gültiger Python-Ausdruck außer einem ungeklammerten Tupel ist und NAME ein Bezeichner ist.
Der Wert eines solchen benannten Ausdrucks ist derselbe wie der integrierte Ausdruck, mit dem zusätzlichen Nebeneffekt, dass das Ziel diesen Wert zugewiesen bekommt
# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
process(chunk)
# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]
# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
Ausnahmefälle
Es gibt einige Stellen, an denen Zuweisungsausdrücke nicht erlaubt sind, um Mehrdeutigkeiten oder Verwirrung der Benutzer zu vermeiden
- Ungeklammerte Zuweisungsausdrücke sind auf der obersten Ebene einer Zuweisungsanweisung verboten. Beispiel
y := f(x) # INVALID (y := f(x)) # Valid, though not recommended
Diese Regel ist enthalten, um die Wahl für den Benutzer zwischen einer Zuweisungsanweisung und einem Zuweisungsausdruck zu vereinfachen – es gibt keine syntaktische Position, an der beide gültig wären.
- Ungeklammerte Zuweisungsausdrücke sind auf der obersten Ebene der rechten Seite einer Zuweisungsanweisung verboten. Beispiel
y0 = y1 := f(x) # INVALID y0 = (y1 := f(x)) # Valid, though discouraged
Auch hier ist diese Regel enthalten, um zwei visuell ähnliche Arten, dasselbe zu sagen, zu vermeiden.
- Ungeklammerte Zuweisungsausdrücke sind für den Wert eines Schlüsselwortarguments in einem Aufruf verboten. Beispiel
foo(x = y := f(x)) # INVALID foo(x=(y := f(x))) # Valid, though probably confusing
Diese Regel ist enthalten, um übermäßig verwirrenden Code zu verbieten und weil das Parsen von Schlüsselwortargumenten ohnehin schon komplex genug ist.
- Ungeklammerte Zuweisungsausdrücke sind für den Standardwert einer Funktion verboten. Beispiel
def foo(answer = p := 42): # INVALID ... def foo(answer=(p := 42)): # Valid, though not great style ...
Diese Regel ist enthalten, um Nebeneffekte an einer Position zu entmutigen, deren genaue Semantik für viele Benutzer bereits verwirrend ist (vgl. die übliche Stil-Empfehlung gegen veränderliche Standardwerte) und um auch das ähnliche Verbot bei Aufrufen (der vorherige Punkt) zu wiederholen.
- Ungeklammerte Zuweisungsausdrücke sind als Anmerkungen für Argumente, Rückgabewerte und Zuweisungen verboten. Beispiel
def foo(answer: p := 42 = 5): # INVALID ... def foo(answer: (p := 42) = 5): # Valid, but probably never useful ...
Die Begründung hierfür ist ähnlich wie bei den beiden vorherigen Fällen; diese ungeordnete Ansammlung von Symbolen und Operatoren, die aus
:und=bestehen, ist schwer korrekt zu lesen. - Ungeklammerte Zuweisungsausdrücke sind in Lambda-Funktionen verboten. Beispiel
(lambda: x := 1) # INVALID lambda: (x := 1) # Valid, but unlikely to be useful (x := lambda: 1) # Valid lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
Dies ermöglicht es
lambda, immer schwächer zu binden als:=; ein Namensbindung auf der obersten Ebene innerhalb einer Lambda-Funktion ist wahrscheinlich nicht nützlich, da es keine Möglichkeit gibt, sie zu nutzen. In Fällen, in denen der Name mehr als einmal verwendet wird, muss der Ausdruck wahrscheinlich ohnehin geklammert werden, sodass dieses Verbot den Code selten beeinträchtigt. - Zuweisungsausdrücke innerhalb von f-Strings erfordern Klammern. Beispiel
>>> f'{(x:=10)}' # Valid, uses assignment expression '10' >>> x = 10 >>> f'{x:=10}' # Valid, passes '=10' to formatter ' 10'
Dies zeigt, dass das, was wie ein Zuweisungsoperator in einem f-String aussieht, nicht immer ein Zuweisungsoperator ist. Der f-String-Parser verwendet
:, um Formatierungsoptionen anzugeben. Um die Abwärtskompatibilität zu wahren, muss die Verwendung des Zuweisungsoperators innerhalb von f-Strings geklammert werden. Wie oben erwähnt, ist diese Verwendung des Zuweisungsoperators nicht empfohlen.
Gültigkeitsbereich des Ziels
Ein Zuweisungsausdruck führt keinen neuen Gültigkeitsbereich ein. In den meisten Fällen ist der Gültigkeitsbereich, in dem das Ziel gebunden wird, selbsterklärend: Es ist der aktuelle Gültigkeitsbereich. Wenn dieser Gültigkeitsbereich eine nonlocal- oder global-Deklaration für das Ziel enthält, beachtet der Zuweisungsausdruck dies. Eine Lambda (die eine explizite, wenn auch anonyme Funktionsdefinition ist) zählt zu diesem Zweck als Gültigkeitsbereich.
Es gibt einen Sonderfall: Ein Zuweisungsausdruck, der in einer Listen-, Mengen- oder Dictionary-Abstraktion oder in einem Generatorausdruck vorkommt (im Folgenden zusammenfassend als „Abstraktionen“ bezeichnet), bindet das Ziel im umschließenden Gültigkeitsbereich und beachtet eine nonlocal- oder global-Deklaration für das Ziel in diesem Gültigkeitsbereich, falls eine vorhanden ist. Für die Zwecke dieser Regel ist der umschließende Gültigkeitsbereich einer verschachtelten Abstraktion der Gültigkeitsbereich, der die äußerste Abstraktion umschließt. Eine Lambda zählt als umschließender Gültigkeitsbereich.
Die Motivation für diesen Sonderfall ist zweifach. Erstens ermöglicht er uns, bequem einen „Zeugen“ für einen any()-Ausdruck oder ein Gegenbeispiel für all() zu erfassen, zum Beispiel
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
if all((nonblank := line).strip() == '' for line in lines):
print("All lines are blank")
else:
print("First non-blank line:", nonblank)
Zweitens ermöglicht er eine kompakte Möglichkeit, veränderliche Zustände aus einer Abstraktion zu aktualisieren, zum Beispiel
# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)
Ein Name des Ziels eines Zuweisungsausdrucks kann jedoch nicht derselbe sein wie ein for-Zielname, der in einer Abstraktion vorkommt, die den Zuweisungsausdruck enthält. Letztere Namen sind lokal für die Abstraktion, in der sie vorkommen, daher wäre es widersprüchlich, wenn ein enthaltener Gebrauch desselben Namens sich auf den Gültigkeitsbereich beziehen würde, der die äußerste Abstraktion stattdessen enthält.
Zum Beispiel ist [i := i+1 for i in range(5)] ungültig: Der Teil for i legt fest, dass i lokal für die Abstraktion ist, aber der Teil i := besteht darauf, dass i nicht lokal für die Abstraktion ist. Aus demselben Grund sind auch diese Beispiele ungültig
[[(j := j) for i in range(5)] for j in range(5)] # INVALID
[i := 0 for i, j in stuff] # INVALID
[i+1 for i in (i := stuff)] # INVALID
Obwohl es technisch möglich ist, diesen Fällen konsistente Semantiken zuzuweisen, ist es schwierig zu bestimmen, ob diese Semantiken tatsächlich *sinnvoll* sind, wenn keine echten Anwendungsfälle vorliegen. Daher wird die Referenzimplementierung [1] sicherstellen, dass solche Fälle SyntaxError auslösen, anstatt mit implementierungsdefinierter Semantik ausgeführt zu werden.
Diese Einschränkung gilt auch dann, wenn der Zuweisungsausdruck nie ausgeführt wird
[False and (i := 0) for i, j in stuff] # INVALID
[i for i, j in stuff if True or (j := 1)] # INVALID
Für den Körper der Abstraktion (der Teil vor dem ersten „for“-Schlüsselwort) und den Filterausdruck (der Teil nach „if“ und vor jedem verschachtelten „for“) gilt diese Einschränkung ausschließlich für Zielnamen, die auch als Iterationsvariablen in der Abstraktion verwendet werden. Lambda-Ausdrücke, die in diesen Positionen vorkommen, führen einen neuen expliziten Funktionsgültigkeitsbereich ein und können daher ohne zusätzliche Einschränkungen Zuweisungsausdrücke verwenden.
Aufgrund von Designbeschränkungen in der Referenzimplementierung (der Symboltabellenanalysator kann nicht einfach erkennen, wann Namen zwischen dem linken Iterable-Ausdruck der Abstraktion und dem Rest der Abstraktion wiederverwendet werden) sind benannte Ausdrücke für den Teil, der nach jedem „in“ und vor jedem nachfolgenden „if“- oder „for“-Schlüsselwort steht, gänzlich nicht zugelassen.
[i+1 for i in (j := stuff)] # INVALID
[i+1 for i in range(2) for j in (k := stuff)] # INVALID
[i+1 for i in [j for j in (k := stuff)]] # INVALID
[i+1 for i in (lambda: (j := stuff))()] # INVALID
Eine weitere Ausnahme gilt, wenn ein Zuweisungsausdruck in einer Abstraktion vorkommt, deren umschließender Gültigkeitsbereich ein Klassen-Gültigkeitsbereich ist. Wenn die obigen Regeln dazu führen würden, dass das Ziel in diesem Klassen-Gültigkeitsbereich zugewiesen wird, ist der Zuweisungsausdruck ausdrücklich ungültig. Dieser Fall löst ebenfalls SyntaxError aus.
class Example:
[(j := i) for i in range(5)] # INVALID
(Der Grund für die letztere Ausnahme ist der implizite Funktionsgültigkeitsbereich, der für Abstraktionen erstellt wird – es gibt derzeit keinen Laufzeitmechanismus für eine Funktion, auf eine Variable im umschließenden Klassen-Gültigkeitsbereich zuzugreifen, und wir wollen einen solchen Mechanismus nicht hinzufügen. Wenn dieses Problem jemals gelöst wird, könnte dieser Sonderfall aus der Spezifikation von Zuweisungsausdrücken entfernt werden. Beachten Sie, dass das Problem bereits für die *Verwendung* einer im Klassen-Gültigkeitsbereich definierten Variable aus einer Abstraktion existiert.)
Siehe Anhang B für einige Beispiele, wie die Regeln für Ziele in Abstraktionen auf äquivalenten Code abgebildet werden.
Relative Priorität von :=
Der Operator := gruppiert in allen syntaktischen Positionen, in denen er legal ist, stärker als ein Komma, aber schwächer als alle anderen Operatoren, einschließlich or, and, not und bedingte Ausdrücke (A if C else B). Wie aus Abschnitt „Ausnahmefälle“ oben hervorgeht, ist er niemals auf derselben Ebene wie = erlaubt. Falls eine andere Gruppierung gewünscht wird, sollten Klammern verwendet werden.
Der Operator := kann direkt als positionales Funktionsargument verwendet werden; er ist jedoch als Schlüsselwortargument direkt ungültig.
Einige Beispiele zur Klärung, was technisch gültig oder ungültig ist
# INVALID
x := 0
# Valid alternative
(x := 0)
# INVALID
x = y := 0
# Valid alternative
x = (y := 0)
# Valid
len(lines := f.readlines())
# Valid
foo(x := 3, cat='vector')
# INVALID
foo(cat=category := 'vector')
# Valid alternative
foo(cat=(category := 'vector'))
Die meisten der oben genannten „gültigen“ Beispiele sind nicht empfehlenswert, da menschliche Leser von Python-Quellcode, die nur kurz einen Code überfliegen, den Unterschied möglicherweise übersehen. Aber einfache Fälle sind nicht zu beanstanden
# Valid
if any(len(longline := line) >= 100 for line in lines):
print("Extremely long line:", longline)
Dieses PEP empfiehlt, immer Leerzeichen um := zu setzen, ähnlich wie die Empfehlung von PEP 8 für = bei Verwendung für Zuweisungen, während letzteres Leerzeichen um = bei Verwendung für Schlüsselwortargumente verbietet.
Änderung der Auswertungsreihenfolge
Um präzise definierte Semantiken zu haben, erfordert der Vorschlag eine gut definierte Auswertungsreihenfolge. Dies ist technisch keine neue Anforderung, da Funktionsaufrufe bereits Nebeneffekte haben können. Python hat bereits die Regel, dass Teilausdrücke im Allgemeinen von links nach rechts ausgewertet werden. Zuweisungsausdrücke machen diese Nebeneffekte jedoch sichtbarer, und wir schlagen eine einzige Änderung der aktuellen Auswertungsreihenfolge vor
- In einer Dictionary-Abstraktion
{X: Y for ...}wird derzeitYvorXausgewertet. Wir schlagen vor, dies zu ändern, sodassXvorYausgewertet wird. (In einer Dictionary-Anzeige wie{X: Y}ist dies bereits der Fall, und auch indict((X, Y) for ...), was eindeutig der Dictionary-Abstraktion entsprechen sollte.)
Unterschiede zwischen Zuweisungsausdrücken und Zuweisungsanweisungen
Am wichtigsten ist, dass := ein Ausdruck ist und daher in Kontexten verwendet werden kann, in denen Anweisungen illegal sind, einschließlich Lambda-Funktionen und Abstraktionen.
Umgekehrt unterstützen Zuweisungsausdrücke nicht die erweiterten Funktionen von Zuweisungsanweisungen
- Mehrere Ziele werden nicht direkt unterstützt
x = y = z = 0 # Equivalent: (z := (y := (x := 0)))
- Einzelne Zuweisungsziele außer einem einzelnen
NAMEwerden nicht unterstützt# No equivalent a[i] = x self.rest = []
- Die Priorität von Kommas ist anders
x = 1, 2 # Sets x to (1, 2) (x := 1, 2) # Sets x to 1
- Iterables-Packen und -Entpacken (sowohl reguläre als auch erweiterte Formen) werden nicht unterstützt
# Equivalent needs extra parentheses loc = x, y # Use (loc := (x, y)) info = name, phone, *rest # Use (info := (name, phone, *rest)) # No equivalent px, py, pz = position name, phone, email, *other_info = contact
- Inline-Typannotationen werden nicht unterstützt
# Closest equivalent is "p: Optional[int]" as a separate declaration p: Optional[int] = None
- Erweiterte Zuweisung wird nicht unterstützt
total += tax # Equivalent: (total := total + tax)
Änderungen der Spezifikation während der Implementierung
Die folgenden Änderungen wurden aufgrund von Implementierungserfahrungen und zusätzlicher Überprüfung vorgenommen, nachdem das PEP zuerst akzeptiert wurde und bevor Python 3.8 veröffentlicht wurde
- aus Konsistenz mit anderen ähnlichen Ausnahmen und um einen Ausnahmetyp zu vermeiden, der nicht unbedingt die Klarheit für Endbenutzer verbessert, wurde die ursprünglich vorgeschlagene
TargetScopeError-Unterklasse vonSyntaxErrorfallen gelassen und stattdessen einfachSyntaxErrordirekt ausgelöst. [3] - Aufgrund einer Einschränkung im Symboltabellenanalyseprozess von CPython löst die Referenzimplementierung
SyntaxErrorfür alle Verwendungen benannter Ausdrücke innerhalb von Abstraktions-Iterable-Ausdrücken aus, anstatt sie nur auszulösen, wenn das Ziel des benannten Ausdrucks mit einer der Iterationsvariablen in der Abstraktion kollidiert. Dies könnte angesichts ausreichend überzeugender Beispiele noch einmal überdacht werden, aber die zusätzliche Komplexität, die zur Implementierung der selektiveren Einschränkung erforderlich ist, scheint für rein hypothetische Anwendungsfälle nicht lohnenswert.
Beispiele
Beispiele aus der Python-Standardbibliothek
site.py
env_base wird nur auf diesen Zeilen verwendet. Die Zuweisung auf der if-Zeile verschiebt sie als „Überschrift“ des Blocks.
- Aktuell
env_base = os.environ.get("PYTHONUSERBASE", None) if env_base: return env_base
- Verbessert
if env_base := os.environ.get("PYTHONUSERBASE", None): return env_base
_pydecimal.py
Vermeiden Sie verschachtelte if-Anweisungen und entfernen Sie eine Einrückungsebene.
- Aktuell
if self._is_special: ans = self._check_nans(context=context) if ans: return ans
- Verbessert
if self._is_special and (ans := self._check_nans(context=context)): return ans
copy.py
Der Code sieht regelmäßiger aus und vermeidet mehrfach verschachtelte if-Anweisungen. (Siehe Anhang A für den Ursprung dieses Beispiels.)
- Aktuell
reductor = dispatch_table.get(cls) if reductor: rv = reductor(x) else: reductor = getattr(x, "__reduce_ex__", None) if reductor: rv = reductor(4) else: reductor = getattr(x, "__reduce__", None) if reductor: rv = reductor() else: raise Error( "un(deep)copyable object of type %s" % cls)
- Verbessert
if reductor := dispatch_table.get(cls): rv = reductor(x) elif reductor := getattr(x, "__reduce_ex__", None): rv = reductor(4) elif reductor := getattr(x, "__reduce__", None): rv = reductor() else: raise Error("un(deep)copyable object of type %s" % cls)
datetime.py
tz wird nur für s += tz verwendet. Die Zuweisung in das if zu verschieben, hilft, seinen Gültigkeitsbereich zu zeigen.
- Aktuell
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) tz = self._tzstr() if tz: s += tz return s
- Verbessert
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) if tz := self._tzstr(): s += tz return s
sysconfig.py
Das Aufrufen von fp.readline() in der while-Bedingung und das Aufrufen von .match() auf den if-Zeilen macht den Code kompakter, ohne ihn schwerer verständlich zu machen.
- Aktuell
while True: line = fp.readline() if not line: break m = define_rx.match(line) if m: n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v else: m = undef_rx.match(line) if m: vars[m.group(1)] = 0
- Verbessert
while line := fp.readline(): if m := define_rx.match(line): n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v elif m := undef_rx.match(line): vars[m.group(1)] = 0
Vereinfachung von Listen-Abstraktionen
Eine Listen-Abstraktion kann effizient abbilden und filtern, indem sie die Bedingung erfasst
results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]
Ähnlich kann ein Teilausdruck innerhalb des Hauptausdrucks wiederverwendet werden, indem ihm bei der ersten Verwendung ein Name gegeben wird
stuff = [[y := f(x), x/y] for x in range(5)]
Beachten Sie, dass in beiden Fällen die Variable y im umschließenden Gültigkeitsbereich gebunden wird (d.h. auf derselben Ebene wie results oder stuff).
Erfassung von Bedingungswerten
Zuweisungsausdrücke können im Kopf einer if- oder while-Anweisung gut eingesetzt werden
# Loop-and-a-half
while (command := input("> ")) != "quit":
print("You entered:", command)
# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
print("Alternate found:", match.group(0))
elif match := re.search(third, text):
print("Fallback found:", match.group(0))
# Reading socket data until an empty string is returned
while data := sock.recv(8192):
print("Received data:", data)
Insbesondere bei der while-Schleife kann dies die Notwendigkeit einer Endlosschleife, einer Zuweisung und einer Bedingung beseitigen. Sie schafft auch eine glatte Parallele zwischen einer Schleife, die einfach einen Funktionsaufruf als Bedingung verwendet, und einer, die diesen als Bedingung verwendet, aber auch den tatsächlichen Wert nutzt.
Gabelung
Ein Beispiel aus der Low-Level-UNIX-Welt
if pid := os.fork():
# Parent code
else:
# Child code
Abgelehnte alternative Vorschläge
Breit gefasste, ähnliche Vorschläge wie dieser sind häufig auf python-ideas aufgetaucht. Unten sind eine Reihe alternativer Schreibweisen aufgeführt, von denen einige speziell für Abstraktionen gedacht sind und die zugunsten der oben genannten verworfen wurden.
Änderung der Gültigkeitsbereichsregeln für Abstraktionen
Eine frühere Version dieses PEP schlug subtile Änderungen der Gültigkeitsbereichsregeln für Abstraktionen vor, um sie im Klassen-Gültigkeitsbereich besser nutzbar zu machen und den Gültigkeitsbereich des „äußersten Iterables“ mit dem Rest der Abstraktion zu vereinheitlichen. Dieser Teil des Vorschlags hätte jedoch zu Rückwärtsinkompatibilitäten geführt und wurde zurückgezogen, damit sich das PEP auf Zuweisungsausdrücke konzentrieren kann.
Alternative Schreibweisen
Grundsätzlich dieselbe Semantik wie der aktuelle Vorschlag, aber anders geschrieben.
EXPR as NAME:stuff = [[f(x) as y, x/y] for x in range(5)]
Da
EXPR as NAMEbereits Bedeutung inimport,exceptundwith-Anweisungen hat (mit unterschiedlicher Semantik), würde dies unnötige Verwirrung stiften oder eine Sonderbehandlung erfordern (z.B. Zuweisungen innerhalb der Kopfzeilen dieser Anweisungen zu verbieten).(Beachten Sie, dass
with EXPR as VARnicht einfach den Wert vonEXPRanVARzuweist – es ruftEXPR.__enter__()auf und weist das Ergebnis *dessen* anVARzu.)Zusätzliche Gründe,
:=gegenüber dieser Schreibweise zu bevorzugen, sind- In
if f(x) as yspringt das Zuweisungsziel nicht ins Auge – es liest sich einfach wieif f x blah blahund ist visuell zu ähnlichif f(x) and y. - In allen anderen Situationen, in denen eine
as-Klausel zulässig ist, erwarten auch Leser mit mittleren Kenntnissen diese Klausel (wenn auch optional), da sie durch das Schlüsselwort, das die Zeile beginnt, eingeleitet wird, und die Grammatik dieses Schlüsselwort eng mit der as-Klausel verknüpft.import foo as barexcept Exc as varwith ctxmgr() as var
Im Gegenteil, der Zuweisungsausdruck gehört nicht zum
ifoderwhile, das die Zeile beginnt, und wir erlauben bewusst auch Zuweisungsausdrücke in anderen Kontexten. - Der parallele Rhythmus zwischen
NAME = EXPRif NAME := EXPR
verstärkt die visuelle Erkennung von Zuweisungsausdrücken.
- In
EXPR -> NAME:stuff = [[f(x) -> y, x/y] for x in range(5)]
Diese Syntax ist inspiriert von Sprachen wie R und Haskell sowie von einigen programmierbaren Taschenrechnern. (Beachten Sie, dass ein nach links gerichteter Pfeil
y <- f(x)in Python nicht möglich ist, da er als Kleiner-als-Zeichen und unärer Minus interpretiert würde.) Diese Syntax hat einen leichten Vorteil gegenüber „as“, da sie nicht mitwith,exceptundimportkollidiert, aber ansonsten äquivalent ist. Sie steht jedoch in keinem Zusammenhang mit Pythons anderer Verwendung von->(Funktionsrückgabe-Typ-Annotationen) und hat im Vergleich zu:=(das aus Algol-58 stammt) eine viel schwächere Tradition.- Dekoration von statement-lokalen Namen mit einem führenden Punkt
stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as" stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="
Dies hat den Vorteil, dass versehentliche Nutzung leicht erkannt werden kann und einige Formen von syntaktischer Mehrdeutigkeit entfernt werden. Dies wäre jedoch die einzige Stelle in Python, an der der Gültigkeitsbereich einer Variablen in ihrem Namen kodiert ist, was die Refaktorierung erschwert.
- Hinzufügen eines
where:zu jeder Anweisung, um lokale Namensbindungen zu erstellenvalue = x**2 + 2*x where: x = spam(1, 4, 7, q)
Die Ausführungsreihenfolge wird umgekehrt (der eingerückte Körper wird zuerst ausgeführt, gefolgt vom „Kopfzeile“). Dies erfordert ein neues Schlüsselwort, es sei denn, ein bestehendes Schlüsselwort wird umfunktioniert (am wahrscheinlichsten
with:). Siehe PEP 3150 für frühere Diskussionen zu diesem Thema (mit dem vorgeschlagenen Schlüsselwortgiven:). TARGET from EXPR:stuff = [[y from f(x), x/y] for x in range(5)]
Diese Syntax hat weniger Konflikte als
as(nur mit derraise Exc from Exc-Notation kollidierend), ist aber ansonsten vergleichbar. Anstattwith expr as target:zu parallelisieren (was nützlich, aber auch verwirrend sein kann), hat dies keine Parallelen, ist aber evokativ.
Sonderfallbehandlung von bedingten Anweisungen
Einer der beliebtesten Anwendungsfälle sind if- und while-Anweisungen. Anstatt einer allgemeineren Lösung erweitert dieser Vorschlag die Syntax dieser beiden Anweisungen um eine Möglichkeit, den verglichenen Wert zu erfassen
if re.search(pat, text) as match:
print("Found:", match.group(0))
Dies funktioniert schön, wenn und NUR wenn die gewünschte Bedingung auf der Wahrhaftigkeit des erfassten Wertes basiert. Er ist somit wirksam für spezifische Anwendungsfälle (Regex-Treffer, Socket-Lesevorgänge, die '' zurückgeben, wenn sie abgeschlossen sind) und völlig nutzlos in komplizierteren Fällen (z.B. wenn die Bedingung f(x) < 0 lautet und man den Wert von f(x) erfassen möchte). Er hat auch keinen Vorteil für Listen-Abstraktionen.
Vorteile: Keine syntaktischen Mehrdeutigkeiten. Nachteile: Beantwortet nur einen Bruchteil der möglichen Anwendungsfälle, selbst in if/while-Anweisungen.
Sonderfallbehandlung von Abstraktionen
Ein weiterer häufiger Anwendungsfall sind Abstraktionen (Listen-/Mengen-/Dictionary-Abstraktionen und Genexps). Wie oben wurden Vorschläge für Abstraktions-spezifische Lösungen gemacht.
where,letodergivenstuff = [(y, x/y) where y = f(x) for x in range(5)] stuff = [(y, x/y) let y = f(x) for x in range(5)] stuff = [(y, x/y) given y = f(x) for x in range(5)]
Dies bringt den Teilausdruck an eine Stelle zwischen der „for“-Schleife und dem Ausdruck. Es führt ein zusätzliches Sprachschlüsselwort ein, was zu Konflikten führt. Von den dreien liest sich
wheream saubersten, hat aber auch das größte Konfliktpotenzial (z.B. haben SQLAlchemy und numpywhere-Methoden, ebenso wietkinter.dnd.Iconin der Standardbibliothek).with NAME = EXPR:stuff = [(y, x/y) with y = f(x) for x in range(5)]
Wie oben, aber unter Wiederverwendung des
with-Schlüsselworts. Liest sich nicht schlecht und benötigt kein zusätzliches Sprachschlüsselwort. Ist jedoch auf Comprehensions beschränkt und kann nicht so leicht in eine „langformige“ for-Schleife umgewandelt werden. Hat das C-Problem, dass ein Gleichheitszeichen in einem Ausdruck nun eine Namensbindung erstellen kann, anstatt einen Vergleich durchzuführen. Würde die Frage aufwerfen, warum „with NAME = EXPR:“ nicht als eigenständige Anweisung verwendet werden kann.with EXPR as NAME:stuff = [(y, x/y) with f(x) as y for x in range(5)]
Wie Option 2, aber unter Verwendung von
asanstelle eines Gleichheitszeichens. Passt syntaktisch zu anderen Verwendungen vonasfür Namensbindungen, aber eine einfache Umwandlung in die langformige for-Schleife würde drastisch andere Semantiken erzeugen; die Bedeutung vonwithinnerhalb einer Comprehension wäre völlig anders als die Bedeutung als eigenständige Anweisung, während die Syntax identisch bliebe.
Unabhängig von der gewählten Schreibweise führt dies zu einem starken Unterschied zwischen Comprehensions und der äquivalenten entrollten langformigen Schleifenform. Es ist nicht mehr möglich, die Schleife in Anweisungsform zu entpacken, ohne Namensbindungen zu überarbeiten. Das einzige Schlüsselwort, das für diese Aufgabe wiederverwendet werden kann, ist with, wodurch es in einer Comprehension heimlich andere Semantiken als in einer Anweisung erhält; alternativ wird ein neues Schlüsselwort benötigt, mit all seinen Kosten.
Senkung der Operatorpriorität
Es gibt zwei logische Prioritäten für den :=-Operator. Entweder sollte er so locker wie möglich binden, wie es die Anweisungszuweisung tut; oder er sollte enger binden als Vergleichsoperatoren. Seine Priorität zwischen den Vergleichs- und arithmetischen Operatoren (genauer gesagt: knapp unterhalb von bitweisem OR) zu platzieren, erlaubt die meisten Verwendungen innerhalb von while- und if-Bedingungen, ohne Klammern zu benötigen, da es am wahrscheinlichsten ist, dass man den Wert von etwas erfassen und dann damit einen Vergleich durchführen möchte.
pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
...
Sobald find() -1 zurückgibt, endet die Schleife. Wenn := so locker bindet wie =, würde dies das Ergebnis des Vergleichs (im Allgemeinen entweder True oder False) erfassen, was weniger nützlich ist.
Während dieses Verhalten in vielen Situationen praktisch wäre, ist es auch schwieriger zu erklären als „der :=-Operator verhält sich genau wie die Zuweisungsanweisung“. Daher wurde die Priorität für := so nah wie möglich an die von = gelegt (mit der Ausnahme, dass sie enger bindet als Komma).
Zulassen von Kommas auf der rechten Seite
Einige Kritiker haben behauptet, dass Zuweisungsausdrücke Tupel auf der rechten Seite ohne Klammern erlauben sollten, sodass diese beiden äquivalent wären
(point := (x, y))
(point := x, y)
(Mit der aktuellen Version des Vorschlags wäre letzteres äquivalent zu ((point := x), y).)
Diese Haltung würde jedoch logisch zu dem Schluss führen, dass Zuweisungsausdrücke bei der Verwendung in einem Funktionsaufruf ebenfalls weniger eng als Komma binden, so dass wir die folgende verwirrende Äquivalenz hätten
foo(x := 1, y)
foo(x := (1, y))
Die weniger verwirrende Option ist, := enger als Komma binden zu lassen.
Immer Klammern erforderlich
Es wurde vorgeschlagen, um einen Zuweisungsausdruck immer Klammern zu setzen. Dies würde viele Mehrdeutigkeiten lösen, und tatsächlich wären Klammern oft notwendig, um den gewünschten Teilausdruck zu extrahieren. Aber in den folgenden Fällen fühlen sich die zusätzlichen Klammern redundant an
# Top level in if
if match := pattern.match(line):
return match.group(1)
# Short call
len(lines := f.readlines())
Häufig gestellte Einwände
Warum nicht einfach bestehende Zuweisung in einen Ausdruck umwandeln?
C und seine Derivate definieren den =-Operator als Ausdruck, anstatt als Anweisung, wie es bei Python üblich ist. Dies erlaubt Zuweisungen in mehr Kontexten, einschließlich Kontexten, in denen Vergleiche häufiger vorkommen. Die syntaktische Ähnlichkeit zwischen if (x == y) und if (x = y) täuscht über ihre drastisch unterschiedlichen Semantiken hinweg. Daher verwendet dieser Vorschlag :=, um die Unterscheidung zu verdeutlichen.
Wenn es Zuweisungsausdrücke gibt, warum sich mit Zuweisungsanweisungen abmühen?
Die beiden Formen haben unterschiedliche Flexibilitäten. Der :=-Operator kann innerhalb eines größeren Ausdrucks verwendet werden; die =-Anweisung kann zu += und seinen Freunden erweitert werden, kann verkettet und kann Attribute und Indizes zuweisen.
Warum nicht einen sublokalen Gültigkeitsbereich verwenden und eine Verschmutzung des Namensraums verhindern?
Frühere Überarbeitungen dieses Vorschlags beinhalteten sublokale Geltungsbereiche (beschränkt auf eine einzelne Anweisung), wodurch Namenslecks und Namensraumverschmutzung verhindert wurden. Während dies in vielen Situationen ein deutlicher Vorteil ist, erhöht es die Komplexität in vielen anderen, und die Kosten sind durch die Vorteile nicht gerechtfertigt. Im Interesse der Sprachvereinfachung sind die hier erstellten Namensbindungen exakt äquivalent zu allen anderen Namensbindungen, einschließlich der Tatsache, dass die Verwendung im Klassen- oder Modulbereich extern sichtbare Namen erzeugt. Dies unterscheidet sich nicht von for-Schleifen oder anderen Konstrukten und kann auf die gleiche Weise gelöst werden: Löschen Sie den Namen mit del, sobald er nicht mehr benötigt wird, oder stellen Sie ihm einen Unterstrich voran.
(Der Autor dankt Guido van Rossum und Christoph Groth für ihre Vorschläge, den Vorschlag in diese Richtung zu lenken. [2])
Stilrichtlinienempfehlungen
Da Ausdruckszuweisungen manchmal äquivalent zu Anweisungszuweisungen verwendet werden können, stellt sich die Frage, welche bevorzugt werden sollte. Zum Vorteil von Stilrichtlinien wie PEP 8 werden zwei Empfehlungen vorgeschlagen.
- Wenn entweder Zuweisungsanweisungen oder Zuweisungsausdrücke verwendet werden können, bevorzugen Sie Anweisungen; sie sind eine klare Absichtserklärung.
- Wenn die Verwendung von Zuweisungsausdrücken zu Mehrdeutigkeiten bezüglich der Ausführungsreihenfolge führt, strukturieren Sie sie stattdessen um, um Anweisungen zu verwenden.
Danksagungen
Die Autoren danken Alyssa Coghlan und Steven D’Aprano für ihre erheblichen Beiträge zu diesem Vorschlag und den Mitgliedern der Mailingliste „core-mentorship“ für ihre Hilfe bei der Implementierung.
Anhang A: Erkenntnisse von Tim Peters
Hier ist ein kurzer Aufsatz, den Tim Peters zu diesem Thema geschrieben hat.
Ich mag keine „beschäftigten“ Codezeilen und mag es auch nicht, konzeptionell zusammenhanglose Logik auf einer einzigen Zeile zu platzieren. So zum Beispiel anstelle von
i = j = count = nerrors = 0
bevorzuge ich
i = j = 0
count = 0
nerrors = 0
stattdessen. Ich vermutete also, dass ich nur wenige Stellen finden würde, an denen ich Zuweisungsausdrücke verwenden wollte. Ich zog sie nicht einmal für Zeilen in Betracht, die bereits die halbe Bildschirmbreite einnahmen. In anderen Fällen war „unrelated“ entscheidend
mylast = mylast[1]
yield mylast[0]
ist eine große Verbesserung gegenüber dem kürzeren
yield (mylast := mylast[1])[0]
Die ursprünglichen beiden Anweisungen tun völlig unterschiedliche konzeptionelle Dinge, und sie zusammenzuwerfen ist konzeptionell unsinnig.
In anderen Fällen machte die Kombination von zusammenhängender Logik das Verständnis schwieriger, z. B. das Umschreiben von
while True:
old = total
total += term
if old == total:
return total
term *= mx2 / (i*(i+1))
i += 2
in das kürzere
while total != (total := total + term):
term *= mx2 / (i*(i+1))
i += 2
return total
Der while-Test dort ist zu subtil und beruht entscheidend auf strikter Links-nach-rechts-Auswertung in einem nicht-short-circuiting- oder Method-Chaining-Kontext. Mein Gehirn ist nicht so verdrahtet.
Aber Fälle wie dieser waren selten. Namensbindung ist sehr häufig, und „sparse ist besser als dicht“ bedeutet nicht „fast leer ist besser als sparse“. Zum Beispiel habe ich viele Funktionen, die None oder 0 zurückgeben, um zu kommunizieren: „Ich habe in diesem Fall nichts Nützliches zurückzugeben, aber da dies oft erwartet wird, werde ich Sie nicht mit einer Ausnahme belästigen.“ Dies ist im Wesentlichen dasselbe wie reguläre Ausdruckssuchfunktionen, die None zurückgeben, wenn keine Übereinstimmung gefunden wird. Daher gab es viel Code der Form
result = solution(xs, n)
if result:
# use result
Ich finde das klarer und sicherlich ein bisschen weniger Tipparbeit und Mustererkennung, da
if result := solution(xs, n):
# use result
Es ist auch schön, ein kleines bisschen horizontalen Leerraum aufzugeben, um eine weitere _Zeile_ umgebenden Codes auf dem Bildschirm zu erhalten. Dies habe ich anfangs nicht stark gewichtet, aber es war so sehr häufig, dass es sich summierte, und ich wurde bald genug genervt, dass ich den kürzeren Code tatsächlich nicht ausführen konnte. Das hat mich überrascht!
Es gibt andere Fälle, in denen Zuweisungsausdrücke wirklich glänzen. Anstatt ein weiteres Beispiel aus meinem Code zu wählen, gab Kirill Balunov ein schönes Beispiel aus der Standardbibliothek copy()-Funktion in copy.py
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
Die immer zunehmende Einrückung ist semantisch irreführend: die Logik ist konzeptionell flach, „der erste Test, der erfolgreich ist, gewinnt“
if reductor := dispatch_table.get(cls):
rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
Einfache Zuweisungsausdrücke ermöglichen es der visuellen Struktur des Codes, die konzeptionelle Flachheit der Logik hervorzuheben; die immer zunehmende Einrückung hat sie verschleiert.
Ein kleineres Beispiel aus meinem Code hat mich begeistert, sowohl weil es inhärent zusammenhängende Logik in eine einzige Zeile bringen ließ als auch weil es eine lästige „künstliche“ Einrückungsebene entfernte
diff = x - x_base
if diff:
g = gcd(diff, n)
if g > 1:
return g
wurde zu
if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
return g
Dieses if ist ungefähr so lang, wie ich meine Zeilen haben möchte, aber es bleibt leicht verständlich.
Insgesamt würde ich in den meisten Zeilen, die einen Namen binden, keine Zuweisungsausdrücke verwenden, aber da dieses Konstrukt so sehr häufig vorkommt, bleiben viele Stellen übrig, an denen ich es tun würde. In den meisten letzteren Fällen habe ich einen kleinen Gewinn gefunden, der sich aufgrund der Häufigkeit summiert, und in den restlichen Fällen habe ich einen moderaten bis großen Gewinn gefunden. Ich würde es sicherlich öfter verwenden als ternäre ifs, aber signifikant seltener als erweiterte Zuweisungen.
Ein numerisches Beispiel
Ich habe noch ein weiteres Beispiel, das mich damals sehr beeindruckt hat.
Wenn alle Variablen positive ganze Zahlen sind und a mindestens so groß ist wie die n-te Wurzel von x, gibt dieser Algorithmus den ganzzahligen Anteil der n-ten Wurzel von x zurück (und verdoppelt ungefähr die Anzahl der genauen Bits pro Iteration).
while a > (d := x // a**(n-1)):
a = ((n-1)*a + d) // n
return a
Es ist nicht offensichtlich, warum das funktioniert, aber auch in der „Loop and a half“-Form nicht offensichtlicher. Es ist schwierig, die Korrektheit zu beweisen, ohne auf die richtige Einsicht (die „Ungleichung vom arithmetischen Mittel und geometrischen Mittel“) aufzubauen und einige nicht-triviale Dinge über das Verhalten verschachtelter Ganzzahlfunktionen zu wissen. Das heißt, die Herausforderungen liegen in der Mathematik und nicht wirklich im Programmieren.
Wenn man all das weiß, dann ist die Zuweisungsausdrucksform leicht als „solange die aktuelle Vermutung zu groß ist, hole eine kleinere Vermutung“ lesbar, wobei die „zu groß?“-Prüfung und die neue Vermutung einen teuren Teilausdruck teilen.
Für meine Augen ist die ursprüngliche Form schwerer zu verstehen
while True:
d = x // a**(n-1)
if a <= d:
break
a = ((n-1)*a + d) // n
return a
Anhang B: Grobe Codeübersetzungen für Abstraktionen
Dieser Anhang versucht, die Regeln für Fälle zu klären (aber nicht zu spezifizieren), in denen ein Ziel in einer Comprehension oder einem Generatorausdruck vorkommt. Für eine Reihe von illustrativen Beispielen zeigen wir den ursprünglichen Code, der eine Comprehension enthält, und die Übersetzung, bei der die Comprehension durch eine äquivalente Generatorfunktion plus etwas Gerüst ersetzt wurde.
Da [x for ...] äquivalent zu list(x for ...) ist, verwenden diese Beispiele ohne Verlust der Allgemeinheit Listen-Comprehensions. Und da diese Beispiele dazu dienen, Randfälle der Regeln zu verdeutlichen, versuchen sie nicht, wie echter Code auszusehen.
Hinweis: Comprehensions werden bereits über die Synthese von verschachtelten Generatorfunktionen wie denen in diesem Anhang implementiert. Der neue Teil ist das Hinzufügen geeigneter Deklarationen zur Festlegung des beabsichtigten Geltungsbereichs von Zuweisungsausdruckszielen (derselbe Geltungsbereich, zu dem sie aufgelöst werden, als ob die Zuweisung im Block der äußersten Comprehension durchgeführt worden wäre). Für Zwecke der Typinferenz implizieren diese illustrativen Erweiterungen nicht, dass Zuweisungsausdrucksziele immer Optional sind (aber sie deuten den Zielbindungsbereich an).
Beginnen wir mit einer Erinnerung daran, welcher Code für einen Generatorausdruck ohne Zuweisungsausdruck generiert wird.
- Ursprünglicher Code (EXPR referenziert normalerweise VAR)
def f(): a = [EXPR for VAR in ITERABLE]
- Übersetzung (machen wir uns keine Sorgen um Namenskonflikte)
def f(): def genexpr(iterator): for VAR in iterator: yield EXPR a = list(genexpr(iter(ITERABLE)))
Fügen wir einen einfachen Zuweisungsausdruck hinzu.
- Ursprünglicher Code
def f(): a = [TARGET := EXPR for VAR in ITERABLE]
- Übersetzung
def f(): if False: TARGET = None # Dead code to ensure TARGET is a local variable def genexpr(iterator): nonlocal TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
Fügen wir eine global TARGET-Deklaration in f() hinzu.
- Ursprünglicher Code
def f(): global TARGET a = [TARGET := EXPR for VAR in ITERABLE]
- Übersetzung
def f(): global TARGET def genexpr(iterator): global TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
Oder fügen wir stattdessen eine nonlocal TARGET-Deklaration in f() hinzu.
- Ursprünglicher Code
def g(): TARGET = ... def f(): nonlocal TARGET a = [TARGET := EXPR for VAR in ITERABLE]
- Übersetzung
def g(): TARGET = ... def f(): nonlocal TARGET def genexpr(iterator): nonlocal TARGET for VAR in iterator: TARGET = EXPR yield TARGET a = list(genexpr(iter(ITERABLE)))
Schließlich verschachteln wir zwei Comprehensions.
- Ursprünglicher Code
def f(): a = [[TARGET := i for i in range(3)] for j in range(2)] # I.e., a = [[0, 1, 2], [0, 1, 2]] print(TARGET) # prints 2
- Übersetzung
def f(): if False: TARGET = None def outer_genexpr(outer_iterator): nonlocal TARGET def inner_generator(inner_iterator): nonlocal TARGET for i in inner_iterator: TARGET = i yield i for j in outer_iterator: yield list(inner_generator(range(3))) a = list(outer_genexpr(range(2))) print(TARGET)
Anhang C: Keine Änderungen an den Gültigkeitsbereichssemantiken
Da dies ein Punkt der Verwirrung war, beachten Sie, dass sich an den Geltungsbereichssemantiken von Python nichts ändert. Funktionslokale Geltungsbereiche werden weiterhin zur Kompilierzeit aufgelöst und haben eine unbegrenzte zeitliche Ausdehnung zur Laufzeit („vollständige Closures“). Beispiel
a = 42
def f():
# `a` is local to `f`, but remains unbound
# until the caller executes this genexp:
yield ((a := i) for i in range(3))
yield lambda: a + 100
print("done")
try:
print(f"`a` is bound to {a}")
assert False
except UnboundLocalError:
print("`a` is not yet bound")
Dann
>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0572.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT