Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

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 derzeit Y vor X ausgewertet. Wir schlagen vor, dies zu ändern, sodass X vor Y ausgewertet wird. (In einer Dictionary-Anzeige wie {X: Y} ist dies bereits der Fall, und auch in dict((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 NAME werden 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 von SyntaxError fallen gelassen und stattdessen einfach SyntaxError direkt ausgelöst. [3]
  • Aufgrund einer Einschränkung im Symboltabellenanalyseprozess von CPython löst die Referenzimplementierung SyntaxError fü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.

  1. EXPR as NAME:
    stuff = [[f(x) as y, x/y] for x in range(5)]
    

    Da EXPR as NAME bereits Bedeutung in import, except und with-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 VAR nicht einfach den Wert von EXPR an VAR zuweist – es ruft EXPR.__enter__() auf und weist das Ergebnis *dessen* an VAR zu.)

    Zusätzliche Gründe, := gegenüber dieser Schreibweise zu bevorzugen, sind

    • In if f(x) as y springt das Zuweisungsziel nicht ins Auge – es liest sich einfach wie if f x blah blah und ist visuell zu ähnlich if 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 bar
      • except Exc as var
      • with ctxmgr() as var

      Im Gegenteil, der Zuweisungsausdruck gehört nicht zum if oder while, das die Zeile beginnt, und wir erlauben bewusst auch Zuweisungsausdrücke in anderen Kontexten.

    • Der parallele Rhythmus zwischen
      • NAME = EXPR
      • if NAME := EXPR

      verstärkt die visuelle Erkennung von Zuweisungsausdrücken.

  2. 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 mit with, except und import kollidiert, 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.

  3. 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.

  4. Hinzufügen eines where: zu jeder Anweisung, um lokale Namensbindungen zu erstellen
    value = 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üsselwort given:).

  5. TARGET from EXPR:
    stuff = [[y from f(x), x/y] for x in range(5)]
    

    Diese Syntax hat weniger Konflikte als as (nur mit der raise Exc from Exc-Notation kollidierend), ist aber ansonsten vergleichbar. Anstatt with 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.

  1. where, let oder given
    stuff = [(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 where am saubersten, hat aber auch das größte Konfliktpotenzial (z.B. haben SQLAlchemy und numpy where-Methoden, ebenso wie tkinter.dnd.Icon in der Standardbibliothek).

  2. 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.

  3. 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 as anstelle eines Gleichheitszeichens. Passt syntaktisch zu anderen Verwendungen von as für Namensbindungen, aber eine einfache Umwandlung in die langformige for-Schleife würde drastisch andere Semantiken erzeugen; die Bedeutung von with innerhalb 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.

  1. Wenn entweder Zuweisungsanweisungen oder Zuweisungsausdrücke verwendet werden können, bevorzugen Sie Anweisungen; sie sind eine klare Absichtserklärung.
  2. 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


Quelle: https://github.com/python/peps/blob/main/peps/pep-0572.rst

Zuletzt geändert: 2025-02-01 08:55:40 GMT