PEP 577 – Erweiterte Zuweisungsausdrücke
- Autor:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Status:
- Zurückgezogen
- Typ:
- Standards Track
- Erstellt:
- 14. Mai 2018
- Python-Version:
- 3.8
- Post-History:
- 22. Mai 2018
Inhaltsverzeichnis
- Rücknahme eines PEP
- Zusammenfassung
- Syntax und Semantik
- Diskussion des Designs
- Zulassen von komplexen Zuweisungszielen
- Erweiterte Zuweisung oder nur Namensbindung?
- Zurückstellung einer Entscheidung über Zieldeklarationen auf Ausdrücksebene
- Ignorieren von benannten Ausdrücken bei der Bestimmung von Zielen für erweiterte Zuweisungen
- Behandlung von Inline-Zuweisungen als Variante der erweiterten Zuweisung
- Verbot von erweiterten Zuweisungen in benannten Ausdrücken auf Klassenebene
- Vergleichsoperatoren vs. Zuweisungsoperatoren
- Beispiele
- Beziehung zu PEP 572
- Danksagungen
- Referenzen
- Urheberrecht
Rücknahme eines PEP
Während der Arbeit an diesem PEP stellte ich fest, dass es nicht wirklich das behandelte, was mich an den vorgeschlagenen Scoping-Regeln von PEP 572 für bisher nicht referenzierte Zuweisungsziele störte, und auch einige signifikante unerwünschte Konsequenzen hatte (am bemerkenswertesten war die Zulassung von >>= und <<= als Inline-Erweiterte-Zuweisungsoperatoren, die etwas völlig anderes bedeuteten als die Vergleichsoperatoren >= und <=).
Ich erkannte auch, dass PEP 572, selbst ohne dedizierte Syntax, technisch Inline-Erweiterte-Zuweisungen unter Verwendung des Moduls operator erlaubt
from operator import iadd
if (target := iadd(target, value)) < limit:
...
Die Beschränkung auf einfache Namen als Inline-Zuweisungsziele bedeutet, dass das Zielausdruck immer ohne Nebeneffekte wiederholt werden kann, und vermeidet somit die Mehrdeutigkeit, die sich aus der Zulassung tatsächlicher eingebetteter erweiterter Zuweisungen ergeben würde (es ist immer noch eine schlechte Idee, da es für Menschen fast sicher schwer zu lesen wäre, dieser Hinweis bezieht sich nur auf die theoretischen Grenzen der Ausdrucksfähigkeit auf Sprachebene).
Dementsprechend zog ich diesen PEP zurück, ohne ihn zur Veröffentlichung einzureichen. Zu dieser Zeit begann ich auch, einen Ersatz-PEP zu schreiben, der sich speziell mit der Handhabung von Zuweisungszielen befasste, die im aktuellen Scope noch nicht als lokale Variablen deklariert worden waren (sowohl für reguläre Block-Scopes als auch für benannte Ausdrücke), aber dieser Entwurf erreichte nie ein Stadium, in dem *ich* ihn besser fand als den letztendlich akzeptierten Vorschlag in PEP 572, so dass er nie irgendwo veröffentlicht und auch keine PEP-Nummer zugewiesen wurde.
Zusammenfassung
Dies ist ein Vorschlag, erweiterte Zuweisungen wie x += 1 als Ausdrücke zuzulassen, nicht nur als Anweisungen.
Als Teil davon wird NAME := EXPR als Inline-Zuweisungsausdruck vorgeschlagen, der die neuen Scoping-Regeln für erweiterte Zuweisungen verwendet, anstatt implizit einen neuen lokalen Variablennamen zu definieren, wie es bestehende Namensbindungsanweisungen tun. Die Frage, ob lokale Variablendeklarationen auf Ausdrücksebene im Funktions-Scope zulässig sind, wird bewusst von der Frage der Zulassung von Namensbindungen auf Ausdrücksebene getrennt und auf einen späteren PEP verschoben.
Dieser PEP ist ein direkter Konkurrent zu PEP 572 (obwohl er stark von der Motivation dieses PEP profitiert und sogar die vorgeschlagene Syntax für Inline-Zuweisungen teilt). Siehe Beziehung zu PEP 572 für weitere Details zu den Verbindungen zwischen den beiden PEPs.
Um die Benutzerfreundlichkeit der neuen Ausdrücke zu verbessern, wird eine semantische Trennung zwischen der Behandlung von erweiterten Zuweisungen in regulären Block-Scopes (Module, Klassen und Funktionen) und der Behandlung von erweiterten Zuweisungen in benannten Ausdrücken (Lambda-Ausdrücke, Generator-Ausdrücke und Comprehensions) vorgeschlagen, so dass alle Inline-Zuweisungen standardmäßig auf den nächstgelegenen umschließenden Block-Scope abzielen.
Ein neuer Kompilierungszeitfehler TargetNameError wird als Unterklasse von SyntaxError hinzugefügt, um Fälle zu behandeln, in denen unklar ist, welches Ziel von einer Inline-Zuweisung gebunden werden soll, oder wenn der Ziel-Scope für die Inline-Zuweisung aus einem anderen Grund ungültig ist.
Syntax und Semantik
Erweiterte Zuweisungsausdrücke
Die Grammatik der Sprache würde angepasst, um die Zulassung von erweiterten Zuweisungen als Ausdrücke zu ermöglichen, wobei das Ergebnis des erweiterten Zuweisungsausdrucks dieselbe Referenz nach der Berechnung ist, die an das gegebene Ziel gebunden wird.
Zum Beispiel:
>>> n = 0
>>> n += 5
5
>>> n -= 2
3
>>> n *= 3
9
>>> n
9
Für veränderliche Ziele bedeutet dies, dass das Ergebnis immer nur das ursprüngliche Objekt ist
>>> seq = []
>>> seq_id = id(seq)
>>> seq += range(3)
[0, 1, 2]
>>> seq_id == id(seq)
True
Erweiterte Zuweisungen an Attribute und Container-Indizes werden zugelassen, wobei das Ergebnis die nach der Berechnung gebundene Referenz ist, genau wie bei einfachen Namenszielen
def increment(self, step=1):
return self._value += step
In diesen Fällen werden __getitem__ und __getattribute__ nach der Zuweisung *nicht* aufgerufen (sie werden nur aufgerufen, um die In-Place-Operation auszuwerten).
Hinzufügen eines Inline-Zuweisungsoperators
Allein durch die Hinzufügung von erweiterten Zuweisungsausdrücken wäre es möglich, ein Symbol wie |= als Allzweck-Zuweisungsoperator zu missbrauchen, indem man einen Target Wrapper-Typ definiert, der wie folgt funktioniert
>>> class Target:
... def __init__(self, value):
... self.value = value
... def __or__(self, other):
... return Target(other)
...
>>> x = Target(10)
>>> x.value
10
>>> x |= 42
<__main__.Target object at 0x7f608caa8048>
>>> x.value
42
Dies ähnelt der Art und Weise, wie das Speichern einer einzelnen Referenz in einer Liste lange als Workaround für das Fehlen eines nonlocal-Schlüsselworts verwendet wurde und auch heute noch verwendet werden kann (in Kombination mit operator.itemsetter), um das Fehlen von Zuweisungen auf Ausdrücksebene zu umgehen.
Anstatt solche Workarounds zu erfordern, schlägt dieser PEP stattdessen vor, die "NAME := EXPR"-Syntax von PEP 572 als neuen Inline-Zuweisungsausdruck zu übernehmen, der die unten beschriebenen Scoping-Regeln für erweiterte Zuweisungen verwendet.
Dies behandelt Fälle, in denen nur der neue Wert von Interesse ist und der zuvor gebundene Wert (falls vorhanden) einfach vollständig verworfen werden kann.
Beachten Sie, dass bei einfachen Namen und komplexen Zuweisungszielen der Inline-Zuweisungsoperator die vorherige Referenz *nicht* liest, bevor er die neue zuweist. Wenn er jedoch im Funktions-Scope verwendet wird (entweder direkt oder innerhalb eines benannten Ausdrucks), definiert er nicht implizit eine neue lokale Variable, sondern löst stattdessen TargetNameError aus (wie unten für erweiterte Zuweisungen beschrieben).
Priorität von Zuweisungsoperatoren
Um die bestehenden Semantiken von erweiterten Zuweisungsanweisungen beizubehalten, werden Inline-Zuweisungsoperatoren so definiert, dass sie eine niedrigere Priorität als alle anderen Operatoren haben, einschließlich des Komma-Pseudoperators. Dies stellt sicher, dass, wenn sie als Top-Level-Ausdruck verwendet werden, die gesamte rechte Seite des Ausdrucks immer noch als der zu verarbeitende Wert interpretiert wird (auch wenn dieser Wert ein Tupel ohne Klammern ist).
Der Unterschied, der sich im Vergleich zu PEP 572 ergibt, ist, dass dort, wo (n := first, second) in PEP 572 n = first setzt, in diesem PEP n = (first, second) gesetzt würde, und um die erste Bedeutung zu erhalten, wären zusätzliche Klammern erforderlich (((n := first), second)).
PEP 572 stellt zu Recht fest, dass dies zu Mehrdeutigkeiten führt, wenn Zuweisungsausdrücke als Funktionsargumente verwendet werden. Dieser PEP löst diese Sorge auf eine andere Weise, indem er verlangt, dass Zuweisungsausdrücke beim Verwenden als Argumente für einen Funktionsaufruf in Klammern gesetzt werden (es sei denn, sie sind das einzige Argument).
Dies ist eine entspanntere Version der Einschränkung für Generator-Ausdrücke (die immer Klammern erfordern, außer wenn sie das einzige Argument eines Funktionsaufrufs sind).
Erweiterte Zuweisung an Namen in Block-Scopes
Es werden keine Änderungen an der Namensbindung von Zielen für erweiterte Zuweisungen in Modul- oder Klass-Scopes vorgeschlagen (dies schließt auch Code ein, der mit "exec" oder "eval" ausgeführt wird). Diese werden weiterhin implizit eine neue lokale Variable als Bindungsziel deklarieren, wie sie es heute tun, und (falls erforderlich) den Namen aus einem äußeren Scope auflösen können, bevor er lokal gebunden wird.
Im Funktions-Scope werden erweiterte Zuweisungen so geändert, dass entweder eine vorherige Namensbindung oder eine Variablendeklaration erforderlich ist, um den Zielnamen explizit als lokal für die Funktion zu etablieren, oder eine explizite global- oder nonlocal-Deklaration. TargetNameError, eine neue Unterklasse von SyntaxError, wird zur Kompilierungszeit ausgelöst, wenn keine solche Bindung oder Deklaration vorhanden ist.
Zum Beispiel würde der folgende Code wie heute kompilieren und ausgeführt werden
x = 0
x += 1 # Sets global "x" to 1
class C:
x += 1 # Sets local "x" to 2, leaves global "x" alone
def local_target():
x = 0
x += 1 # Sets local "x" to 1, leaves global "x" alone
def global_target():
global x
x += 1 # Increments global "x" each time this runs
def nonlocal_target():
x = 0
def g():
nonlocal x
x += 1 # Increments "x" in outer scope each time this runs
return x
return g
Die folgenden Beispiele würden immer noch kompiliert und dann zur Laufzeit einen Fehler auslösen, wie sie es heute tun
n += 1 # Raises NameError at runtime
class C:
n += 1 # Raises NameError at runtime
def missing_global():
global n
n += 1 # Raises NameError at runtime
def delayed_nonlocal_initialisation():
def f():
nonlocal n
n += 1
f() # Raises NameError at runtime
n = 0
def skipped_conditional_initialisation():
if False:
n = 0
n += 1 # Raises UnboundLocalError at runtime
def local_declaration_without_initial_assignment():
n: typing.Any
n += 1 # Raises UnboundLocalError at runtime
Während die folgenden zur Kompilierungszeit zunächst eine DeprecationWarning auslösen und schließlich zu TargetNameError werden würden
def missing_target():
x += 1 # Compile time TargetNameError due to ambiguous target scope
# Is there a missing initialisation of "x" here? Or a missing
# global or nonlocal declaration?
Als konservativer Implementierungsansatz würde die Änderung der Namensauflösung zur Kompilierungszeit in Python 3.8 als DeprecationWarning eingeführt und dann in Python 3.9 zu TargetNameError konvertiert. Dies vermeidet potenzielle Probleme in Fällen, in denen eine ungenutzte Funktion derzeit UnboundLocalError auslösen würde, wenn sie jemals tatsächlich aufgerufen würde, aber der Code tatsächlich ungenutzt ist - die Umwandlung dieses latenten Laufzeitfehlers in einen Kompilierungszeitfehler qualifiziert als abwärtskompatible Änderung, die eine Deputationsperiode erfordert.
Wenn erweiterte Zuweisungen als Ausdrücke im Funktions-Scope verwendet werden (anstatt als eigenständige Anweisungen), gibt es keine Probleme mit der Abwärtskompatibilität, so dass die Namensbindungsprüfungen zur Kompilierungszeit sofort in Python 3.8 erzwungen würden.
Ebenso würden die neuen Inline-Zuweisungsausdrücke ab Python 3.8 immer eine explizite Vordeklaration ihres Ziel-Scopes erfordern, wenn sie als Teil einer Funktion verwendet werden. (Siehe den Abschnitt zur Design-Diskussion für Hinweise zur möglichen Überprüfung dieser Einschränkung in der Zukunft).
Erweiterte Zuweisung an Namen in benannten Ausdrücken
Benannte Ausdrücke ist ein neuer Sammelbegriff, der für Ausdrücke vorgeschlagen wird, die einen neuen geschachtelten Ausführungsumfang einführen, entweder als intrinsischer Teil ihrer Operation (Lambda-Ausdrücke, Generator-Ausdrücke) oder als Möglichkeit, Namensbindungsoperationen aus dem umschließenden Scope zu verbergen (Container-Comprehensions).
Im Gegensatz zu regulären Funktionen können diese benannten Ausdrücke keine expliziten global- oder nonlocal-Deklarationen enthalten, um Namen direkt in einem äußeren Scope neu zu binden.
Stattdessen werden ihre Namensbindungssemantiken für erweiterte Zuweisungsausdrücke wie folgt definiert
- Erweiterte Zuweisungsziele, die in benannten Ausdrücken verwendet werden, sollen entweder im umschließenden Block-Scope bereits gebunden sein oder ihr Scope muss im umschließenden Block-Scope explizit deklariert sein. Wenn in diesem Scope keine geeignete Namensbindung oder Deklaration gefunden werden kann, wird zur Kompilierungszeit
TargetNameErrorausgelöst (anstatt eine neue Bindung innerhalb des benannten Ausdrucks zu erstellen). - Wenn der umschließende Block-Scope ein Funktions-Scope ist und der Zielname explizit als
globalodernonlocaldeklariert ist, dann wird dieselbe Scope-Deklaration im Körper des benannten Ausdrucks verwendet - Wenn der umschließende Block-Scope ein Funktions-Scope ist und der Zielname eine lokale Variable in dieser Funktion ist, dann wird er implizit als
nonlocalim Körper des benannten Ausdrucks deklariert - Wenn der umschließende Block-Scope ein Klassen-Scope ist, wird immer
TargetNameErrorausgelöst, mit einer speziellen Meldung, die darauf hinweist, dass die Kombination von Klassen-Scopes mit erweiterten Zuweisungen in benannten Ausdrücken derzeit nicht zulässig ist. - Wenn ein Name als formales Argument (Lambda-Ausdrücke) oder als Iterationsvariable (Generator-Ausdrücke, Comprehensions) deklariert wird, dann gilt dieser Name als lokal für diesen benannten Ausdruck, und der Versuch, ihn als Ziel einer erweiterten Zuweisungsoperation in diesem Scope oder einem geschachtelten benannten Ausdruck zu verwenden, löst
TargetNameErroraus (dies ist eine Einschränkung, die später möglicherweise aufgehoben wird, aber vorerst vorgeschlagen wird, um die anfänglichen Kompilierungszeit- und Laufzeitsemantiken zu vereinfachen, die im Sprachreferenzhandbuch abgedeckt und vom Compiler und Interpreter behandelt werden müssen).
Zum Beispiel würde der folgende Code wie gezeigt funktionieren
>>> global_target = 0
>>> incr_global_target = lambda: global_target += 1
>>> incr_global_target()
1
>>> incr_global_target()
2
>>> global_target
2
>>> def cumulative_sums(data, start=0)
... total = start
... yield from (total += value for value in data)
... return total
...
>>> print(list(cumulative_sums(range(5))))
[0, 1, 3, 6, 10]
Während die folgenden Beispiele alle TargetNameError auslösen würden
class C:
cls_target = 0
incr_cls_target = lambda: cls_target += 1 # Error due to class scope
def missing_target():
incr_x = lambda: x += 1 # Error due to missing target "x"
def late_target():
incr_x = lambda: x += 1 # Error due to "x" being declared after use
x = 1
lambda arg: arg += 1 # Error due to attempt to target formal parameter
[x += 1 for x in data] # Error due to attempt to target iteration variable
Da erweiterte Zuweisungen derzeit nicht innerhalb von benannten Ausdrücken erscheinen können, wären die oben genannten Namensauflösungsfehler zur Kompilierungszeit Teil der anfänglichen Implementierung, anstatt als potenziell abwärtsinkompatible Änderung eingeführt zu werden.
Diskussion des Designs
Zulassen von komplexen Zuweisungszielen
Die ersten Entwürfe dieses PEP behielten die Einschränkung von PEP 572 auf einzelne Namensziele bei, wenn erweiterte Zuweisungen als Ausdrücke verwendet wurden, und erlaubten Attribut- und Indexziele nur für die Statement-Form.
Die Durchsetzung dieser Regel erforderte jedoch eine Variation der zulässigen Ziele, abhängig davon, ob die erweiterte Zuweisung ein Top-Level-Ausdruck war oder nicht, und erklärte auch, warum n += 1, (n += 1) und self.n += 1 alle gültig waren, aber (self.n += 1) verboten war, so dass der Vorschlag vereinfacht wurde, um alle bestehenden erweiterten Zuweisungsziele auch für die Ausdrucksform zuzulassen.
Da dieser PEP TARGET := EXPR als Variante der erweiterten Zuweisung definiert, gewann dies auch Unterstützung für Zuweisungs- und Indexziele.
Erweiterte Zuweisung oder nur Namensbindung?
PEP 572 macht eine vernünftige Argumentation dafür, dass die potenziellen Anwendungsfälle für Inline-Erweiterte-Zuweisung deutlich schwächer sind als die für Inline-Zuweisung im Allgemeinen, so dass es akzeptabel ist, zu verlangen, dass sie als x := x + 1 geschrieben werden, wodurch alle In-Place-Erweiterte-Zuweisungsmethoden umgangen werden.
Während dies zumindest für die eingebauten Typen argumentierbar ist (wo potenzielle Gegenbeispiele sich wahrscheinlich auf Mengenmanipulationsanwendungsfälle konzentrieren müssten, die der PEP-Autor persönlich nicht hat), würde dies auch speichereffizientere Anwendungsfälle wie die Manipulation von NumPy-Arrays ausschließen, bei denen die Datenkopie, die bei Out-of-Place-Operationen anfällt, diese als Alternativen zu ihren In-Place-Gegenstücken unpraktisch machen kann.
Dennoch existiert dieser PEP hauptsächlich, weil der PEP-Autor den Vorschlag für Inline-Zuweisungen viel einfacher als „Es ist wie +=, nur ohne den Additionschritt“ auffasste und auch mochte, wie diese Formulierung einen tatsächlichen semantischen Unterschied zwischen NAME = EXPR und NAME := EXPR im Funktions-Scope bietet.
Dieser Unterschied im Target-Scoping-Verhalten bedeutet, dass die NAME := EXPR-Syntax voraussichtlich zwei Hauptanwendungsfälle haben würde
- als Möglichkeit, Zuweisungen als Ausdruck in einer
if- oderwhile-Anweisung oder als Teil eines benannten Ausdrucks einzubetten - als Möglichkeit, eine Kompilierungszeitprüfung anzufordern, dass der Zielname zuvor im aktuellen Funktions-Scope deklariert oder gebunden wurde
Auf Modul- oder Klass-Scope wären NAME = EXPR und NAME := EXPR semantisch äquivalent, aufgrund der mangelnden Sichtbarkeit des Compilers in die Menge der zur Laufzeit auflösbaren Namen. Code-Linter und statische Typprüfer würden jedoch ermutigt, für NAME := EXPR dieselbe "Deklaration oder Zuweisung vor Verwendung"-Verhaltensweise zu erzwingen, wie der Compiler im Funktions-Scope erzwingen würde.
Zurückstellung einer Entscheidung über Zieldeklarationen auf Ausdrücksebene
Zumindest für Python 3.8 würde die Verwendung von Inline-Zuweisungen (ob erweitert oder nicht) im Funktions-Scope immer eine vorherige Namensbindung oder Scope-Deklaration erfordern, um TargetNameError zu vermeiden, auch wenn sie außerhalb eines benannten Ausdrucks verwendet wird.
Die Absicht hinter dieser Anforderung ist, die folgenden beiden Fragen zur Sprachgestaltung klar zu trennen
- Kann ein Ausdruck einen Namen im aktuellen Scope neu binden?
- Kann ein Ausdruck einen neuen Namen im aktuellen Scope deklarieren?
Für globale Modul-Scopes ist die Antwort auf beide Fragen eindeutig "Ja", da es eine Garantie auf Sprachebene gibt, dass die Mutation des globals()-Dicts sofort den Laufzeit-Modul-Scope beeinflusst, und global NAME-Deklarationen innerhalb einer Funktion denselben Effekt haben können (ebenso wie der aktuell ausgeführte Modul importiert und seine Attribute modifiziert werden).
Für Klassen-Scopes ist die Antwort auf beide Fragen ebenfalls "Ja" in der Praxis, wenn auch weniger eindeutig, da die Semantiken von locals() derzeit formell nicht spezifiziert sind. Wenn jedoch das aktuelle Verhalten von locals() im Klassen-Scope als normativ angesehen wird (wie von PEP 558 vorgeschlagen), dann ist dies im Wesentlichen dasselbe Szenario wie die Manipulation der Modul-Globals, nur mit locals() anstelle.
Für Funktions-Scopes sind die aktuellen Antworten auf diese beiden Fragen jedoch jeweils "Ja" und "Nein". Die Neuzuweisung von Funktionslokalen auf Ausdrücksebene ist dank lexikalisch verschachtelter Scopes und expliziter nonlocal NAME-Ausdrücke bereits möglich. Obwohl dieser PEP die Neuzuweisung auf Ausdrücksebene wahrscheinlich häufiger machen wird als heute, ist dies kein grundlegend neues Konzept für die Sprache.
Im Gegensatz dazu ist die Deklaration einer *neuen* lokalen Funktionsvariable derzeit eine Anweisungsaktion, die eine der folgenden Optionen beinhaltet
- eine Zuweisungsanweisung (
NAME = EXPR,OTHER_TARGET = NAME = EXPR, etc.) - eine Variablendeklaration (
NAME : EXPR) - eine verschachtelte Funktionsdefinition
- eine verschachtelte Klassendefinition
- eine
for-Schleife - eine
with-Anweisung - eine
except-Klausel (mit eingeschränktem Zugriffsbereich)
Der historische Trend für die Sprache war tatsächlich, die Unterstützung für Deklarationen von Funktionslokalen auf Ausdrücksebene zu *entfernen*, zuerst mit der Einführung von "fast locals"-Semantiken (die die Einführung von Namen über locals() für Funktions-Scopes nicht unterstützten) und erneut mit dem Verbergen von Iterationsvariablen von Comprehensions in Python 3.0.
Nun, es könnte sein, dass wir in Python 3.9 diese Frage auf der Grundlage unserer Erfahrungen mit Namensbindungen auf Ausdrücksebene in Python 3.8 erneut prüfen und entscheiden, dass wir tatsächlich Deklarationen von lokalen Funktionsvariablen auf Ausdrücksebene wollen, und dass wir NAME := EXPR als Schreibweise dafür verwenden wollen (anstatt zum Beispiel Inline-Deklarationen expliziter als NAME := EXPR given NAME zu schreiben, was ihnen erlauben würde, Typannotationen zu tragen, und auch erlauben würde, neue lokale Variablen in benannten Ausdrücken zu deklarieren, anstatt den Namensraum in ihrem umschließenden Scope zu verschmutzen).
Aber der Vorschlag in diesem PEP ist, dass wir uns bewusst eine vollständige Version geben, um zu entscheiden, wie sehr wir dieses Feature wollen und wo genau wir sein Fehlen als störend empfinden. Python hat jahrzehntelang glücklich ohne Namensbindungen *oder* Deklarationen auf Ausdrücksebene überlebt, so dass wir uns ein paar Jahre Zeit nehmen können, um zu entscheiden, ob wir wirklich *beides* wollen, oder ob Namensbindungen auf Ausdrücksebene ausreichen.
Ignorieren von benannten Ausdrücken bei der Bestimmung von Zielen für erweiterte Zuweisungen
Bei der Diskussion möglicher Bindungssemantiken für PEP 572s Zuweisungsausdrücke machte Tim Peters einen plausiblen Fall [1], [2], [3] dafür, dass Zuweisungsausdrücke auf den umschließenden Block-Scope abzielen, und dabei dazwischenliegende benannte Ausdrücke im Wesentlichen ignorieren.
Dieser Ansatz ermöglicht es, Anwendungsfälle wie kumulative Summen oder das Extrahieren des Endwerts aus einem Generator-Ausdruck relativ einfach zu schreiben
total = 0
partial_sums = [total := total + value for value in data]
factor = 1
while any(n % (factor := p) == 0 for p in small_primes):
n //= factor
Guido äußerte sich ebenfalls zustimmend zu diesem allgemeinen Ansatz [4].
Der Vorschlag in diesem PEP unterscheidet sich in drei Hauptbereichen von Tims ursprünglichem Vorschlag
- er wendet den Vorschlag auf alle erweiterten Zuweisungsoperatoren an, nicht nur auf einen einzigen neuen Namensbindungsoperator
- soweit praktisch möglich, erweitert er die Anforderung der erweiterten Zuweisung, dass der Name bereits definiert sein muss, auf den neuen Namensbindungsoperator (der
TargetNameErrorauslöst, anstatt implizit neue lokale Variablen im Funktions-Scope zu deklarieren) - er schließt Lambda-Ausdrücke in die Menge der Scopes ein, die für die Namensbindungsziele ignoriert werden, wodurch diese Transparenz für Zuweisungen für alle benannten Ausdrücke üblich wird, anstatt auf Comprehensions und Generator-Ausdrücke beschränkt zu sein
Da benannte Ausdrücke bei der Berechnung von Bindungszielen ignoriert werden, ist es wieder schwierig, den Scoping-Unterschied zwischen den äußersten Iterable-Ausdrücken in Generator-Ausdrücken und Comprehensions zu erkennen (man muss entweder mit Klassen-Scopes herumspielen oder versuchen, Iterationsvariablen neu zu binden, um ihn zu erkennen), so dass es auch keinen Bedarf gibt, dies zu ändern.
Behandlung von Inline-Zuweisungen als Variante der erweiterten Zuweisung
Eine der Herausforderungen bei PEP 572 ist die Tatsache, dass NAME = EXPR und NAME := EXPR in jedem Scope semantisch äquivalent sind. Dies macht die beiden Formen schwer zu lehren, da es keinen inhärenten Anreiz gibt, eine gegenüber der anderen auf Anweisungsebene zu wählen, so dass man sich auf " NAME = EXPR wird bevorzugt, da es schon länger existiert" zurückziehen muss (und PEP 572 vorschlägt, diese historische Eigenart auf Compiler-Ebene durchzusetzen).
Diese semantische Äquivalenz ist auf Modul- und Klass-Scope schwer zu vermeiden, während if NAME := EXPR: und while NAME := EXPR: sinnvoll funktionieren. Im Funktions-Scope macht jedoch die umfassende Sicht des Compilers auf alle lokalen Namen es möglich, zu verlangen, dass der Name vor der Verwendung zugewiesen oder deklariert wird, was einen vernünftigen Anreiz bietet, bei Möglichkeit weiterhin die Form NAME = EXPR zu verwenden, während auch die Verwendung von NAME := EXPR als eine Art einfache Kompilierungszeit-Assertion ermöglicht wird (d.h. explizit anzuzeigen, dass der Zielname bereits gebunden oder deklariert wurde und daher dem Compiler bereits bekannt sein sollte).
Wenn Guido erklären würde, dass die Unterstützung für Inline-Deklarationen eine harte Designanforderung sei, dann würde dieser PEP aktualisiert, um vorzuschlagen, dass EXPR given NAME ebenfalls als Möglichkeit zur Unterstützung von Inline-Namensdeklarationen nach beliebigen Ausdrücken eingeführt wird (dies würde es ermöglichen, die Inline-Namensdeklarationen bis zum Ende eines komplexen Ausdrucks zu verzögern, anstatt sie in die Mitte einbetten zu müssen, und PEP 8 würde eine Empfehlung erhalten, die diesen Stil fördert).
Verbot von erweiterten Zuweisungen in benannten Ausdrücken auf Klassenebene
Während moderne Klassen eine implizite Closure definieren, die für Methodenimplementierungen sichtbar ist (um __class__ für Null-Argument-super()-Aufrufe verfügbar zu machen), gibt es keine Möglichkeit für Benutzercode, zusätzliche Namen explizit zu diesem Scope hinzuzufügen.
In der Zwischenzeit werden Attribute, die in einem Klassenrumpf definiert sind, für die Definition der lexikalischen Closure einer Methode ignoriert, was bedeutet, dass ihre Hinzufügung dort nicht auf Implementierungsebene funktionieren würde.
Anstatt diese inhärente Mehrdeutigkeit zu lösen, verbietet dieser PEP einfach die Verwendung und verlangt, dass jede betroffene Logik woanders geschrieben wird als direkt inline im Klassenrumpf (z.B. in einer separaten Hilfsfunktion).
Vergleichsoperatoren vs. Zuweisungsoperatoren
Der OP=-Konstrukt als Ausdruck stellt derzeit eine Vergleichsoperation dar
x == y # Equals
x >= y # Greater-than-or-equal-to
x <= y # Less-than-or-equal-to
Sowohl dieser PEP als auch PEP 572 schlagen vor, mindestens einen Operator hinzuzufügen, der im Aussehen einigermaßen ähnlich ist, aber stattdessen eine Zuweisung definiert
x := y # Becomes
Dieser PEP geht dann viel weiter und erlaubt alle *13* erweiterten Zuweisungssymbole als binäre Operatoren zu verwenden
x += y # In-place add
x -= y # In-place minus
x *= y # In-place multiply
x @= y # In-place matrix multiply
x /= y # In-place division
x //= y # In-place int division
x %= y # In-place mod
x &= y # In-place bitwise and
x |= y # In-place bitwise or
x ^= y # In-place bitwise xor
x <<= y # In-place left shift
x >>= y # In-place right shift
x **= y # In-place power
Von diesen zusätzlichen binären Operatoren wären die am fragwürdigsten die Bitshift-Zuweisungsoperatoren, da sie jeweils nur ein doppeltes Zeichen von einem der inklusiven geordneten Vergleichsoperatoren entfernt sind.
Beispiele
Vereinfachung von Retry-Schleifen
Es gibt derzeit verschiedene Optionen für das Schreiben von Retry-Schleifen, darunter
# Post-decrementing a counter
remaining_attempts = MAX_ATTEMPTS
while remaining_attempts:
remaining_attempts -= 1
try:
result = attempt_operation()
except Exception as exc:
continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success!
else:
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
# Loop-and-a-half with a pre-incremented counter
attempt = 0
while True:
attempts += 1
if attempts > MAX_ATTEMPTS:
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
try:
result = attempt_operation()
except Exception as exc:
continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success!
Jede der verfügbaren Optionen verbirgt einen Aspekt der beabsichtigten Schleifenstruktur innerhalb des Schleifenkörpers, sei es die Zustandsänderung, die Abbruchbedingung oder beides.
Der Vorschlag in diesem PEP ermöglicht es, sowohl die Zustandsänderung als auch die Abbruchbedingung direkt in die Schleifenüberschrift aufzunehmen
attempt = 0
while (attempt += 1) <= MAX_ATTEMPTS:
try:
result = attempt_operation()
except Exception as exc:
continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success!
else:
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
Vereinfachung von if-elif-Ketten
if-elif-Ketten, die die geprüfte Bedingung neu binden müssen, müssen derzeit mit verschachtelten if-else-Anweisungen geschrieben werden
m = pattern.match(data)
if m:
...
else:
m = other_pattern.match(data)
if m:
...
else:
m = yet_another_pattern.match(data)
if m:
...
else:
...
Wie bei PEP 572 ermöglicht dieser PEP die Kondensation der else/if-Teile dieser Kette, wodurch ihre konsistente und sich gegenseitig ausschließende Struktur besser erkennbar wird
m = pattern.match(data)
if m:
...
elif m := other_pattern.match(data):
...
elif m := yet_another_pattern.match(data):
...
else:
...
Im Gegensatz zu PEP 572 verlangt dieser PEP, dass das Zuweisungsziel vor der ersten Verwendung als :=-Ziel explizit als lokal gekennzeichnet wird, entweder durch Bindung an einen Wert (wie oben gezeigt) oder durch Einbeziehung einer entsprechenden expliziten Typdeklaration.
m: typing.re.Match
if m := pattern.match(data):
...
elif m := other_pattern.match(data):
...
elif m := yet_another_pattern.match(data):
...
else:
...
Erfassung von Zwischenwerten aus Comprehensions
Der Vorschlag in diesem PEP macht es einfach, Zwischenwerte in Comprehensions und Generator-Ausdrücken zu erfassen und wiederzuverwenden, indem sie in den umschließenden Block-Scope exportiert werden
factor: int
while any(n % (factor := p) == 0 for p in small_primes):
n //= factor
total = 0
partial_sums = [total += value for value in data]
Zulassen von Lambda-Ausdrücken, die sich mehr wie wiederverwendbare Code-Thunks verhalten
Dieser PEP erlaubt das klassische Closure-Anwendungsbeispiel
def make_counter(start=0):
x = start
def counter(step=1):
nonlocal x
x += step
return x
return counter
Abzukürzen als
def make_counter(start=0):
x = start
return lambda step=1: x += step
Während die letztere Form immer noch ein konzeptionell dichtes Code-Stück ist, kann vernünftigerweise argumentiert werden, dass das Fehlen von Boilerplate (wo die Schlüsselwörter "def", "nonlocal" und "return" sowie zwei zusätzliche Wiederholungen des Variablennamens "x" durch das Schlüsselwort "lambda" ersetzt wurden) es in der Praxis leichter lesbar machen kann.
Beziehung zu PEP 572
Die Argumentation für die Zulassung von Inline-Zuweisungen im Allgemeinen wird in PEP 572 dargelegt. Dieser konkurrierende PEP würde zunächst eine alternative Oberflächensyntax vorschlagen (EXPR given NAME = EXPR), während die Ausdruckssemantik von PEP 572 beibehalten wird, aber das änderte sich, als einer der anfänglichen motivierenden Anwendungsfälle für die Zulassung eingebetteter Zuweisungen diskutiert wurde: die Möglichkeit, einfache kumulative Summen in Comprehensions und Generator-Ausdrücken zu berechnen.
Als Ergebnis davon und im Gegensatz zu PEP 572 konzentriert sich dieser PEP hauptsächlich auf Anwendungsfälle für Inline-Erweiterte-Zuweisung. Er hat auch den Effekt, Fälle, die derzeit unweigerlich UnboundLocalError zur Laufzeit des Funktionsaufrufs auslösen, in einen neuen Kompilierungszeitfehler TargetNameError umzuwandeln.
Neue Syntax für einen Namensneubindungsausdruck (NAME := TARGET) wird dann nicht nur hinzugefügt, um dieselben Anwendungsfälle wie in PEP 572 identifiziert zu behandeln, sondern auch als untergeordnete Primitive, um die neuen erweiterten Zuweisungssemantiken zu veranschaulichen, zu implementieren und zu erklären, anstatt die einzige vorgeschlagene Änderung zu sein.
Der Autor dieses PEP glaubt, dass dieser Ansatz den Wert der neuen Flexibilität bei der Umbenennung von Namen deutlicher macht und gleichzeitig viele der potenziellen Bedenken im Zusammenhang mit PEP 572 mildert, die sich auf die Erklärung beziehen, wann NAME = EXPR gegenüber NAME := EXPR (und umgekehrt) verwendet werden sollte, ohne die bloße Anweisungsform von NAME := EXPR vollständig zu verbieten (so dass NAME := EXPR ein Kompilierungsfehler ist, aber (NAME := EXPR) erlaubt ist).
Danksagungen
Der PEP-Autor möchte Chris Angelico für seine Arbeit an PEP 572 und seine Bemühungen danken, eine kohärente Zusammenfassung der vielen weitläufigen Diskussionen zu erstellen, die sowohl auf python-ideas als auch auf python-dev stattfanden, sowie Tim Peters für die eingehende Diskussion über lokale Geltungsbereiche von Klammern, die den oben genannten Geltungsbereichsvorschlag für erweiterte Zuweisungen innerhalb von auswertbaren Ausdrücken veranlasste.
Eric Snows Feedback zu einer Vorabversion dieses PEP hat dazu beigetragen, ihn deutlich lesbarer zu machen.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0577.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT