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

Python Enhancement Proposals

PEP 403 – Allgemeine Decorator-Klausel (auch „@in“-Klausel)

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Verschoben
Typ:
Standards Track
Erstellt:
13-Okt-2011
Python-Version:
3.4
Post-History:
13-Okt-2011

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt die Hinzufügung einer neuen @in-Decorator-Klausel vor, die es ermöglicht, den Namensbindeschritt einer Funktions- oder Klassendefinition zu überschreiben.

Die neue Klausel akzeptiert eine einzelne einfache Anweisung, die eine Vorwärtsreferenz auf die dekorierte Funktions- oder Klassendefinition machen kann.

Diese neue Klausel ist dafür gedacht, immer dann verwendet zu werden, wenn eine „Einmal“-Funktion oder -Klasse benötigt wird und die Platzierung der Funktions- oder Klassendefinition vor der Anweisung, die sie verwendet, den Code tatsächlich schwerer lesbar macht. Sie vermeidet auch Bedenken hinsichtlich Namensüberschneidungen, indem sie sicherstellt, dass der neue Name nur für die Anweisung in der @in-Klausel sichtbar ist.

Diese PEP basiert stark auf vielen Ideen aus PEP 3150 (Statement Local Namespaces), sodass einige Elemente der Begründung für Leser dieser PEP vertraut sein werden. Beide PEPs bleiben vorerst zurückgestellt, hauptsächlich aufgrund des Fehlens zwingender realer Anwendungsfälle in beiden PEPs.

Grundlegende Beispiele

Bevor wir uns in die lange Geschichte dieses Problems und die detaillierte Begründung für diese spezifische Lösung stürzen, hier ein paar einfache Beispiele für die Art von Code, die sie vereinfachen soll.

Als triviales Beispiel könnte ein Weakref-Callback wie folgt definiert werden

@in x = weakref.ref(target, report_destruction)
def report_destruction(obj):
    print("{} is being destroyed".format(obj))

Dies steht im Gegensatz zur aktuellen (konzeptionellen) „out of order“-Syntax für diese Operation

def report_destruction(obj):
    print("{} is being destroyed".format(obj))

x = weakref.ref(target, report_destruction)

Diese Struktur ist in Ordnung, wenn Sie den aufrufbaren Wert mehrmals verwenden, aber es ist ärgerlich, ihn für Einmaloperationen erzwingen zu müssen.

Wenn die Wiederholung des Namens besonders ärgerlich erscheint, kann stattdessen ein Wegwerfname wie f verwendet werden

@in x = weakref.ref(target, f)
def f(obj):
    print("{} is being destroyed".format(obj))

Ähnlich könnte eine sortierte Operation auf einem besonders schlecht definierten Typ jetzt wie folgt definiert werden

@in sorted_list = sorted(original, key=f)
def f(item):
    try:
        return item.calc_sort_order()
    except NotSortableError:
        return float('inf')

Anstatt

def force_sort(item):
    try:
        return item.calc_sort_order()
    except NotSortableError:
        return float('inf')

sorted_list = sorted(original, key=force_sort)

Und frühe Bindungssemantiken in einer Listen-Comprehension könnten über folgenden Weg erreicht werden

@in funcs = [adder(i) for i in range(10)]
def adder(i):
    return lambda x: x + i

Vorschlag

Diese PEP schlägt die Hinzufügung einer neuen @in-Klausel vor, die eine Variante der bestehenden Klassen- und Funktions-Decorator-Syntax ist.

Die neue @in-Klausel geht den Decorator-Zeilen voraus und erlaubt Vorwärtsreferenzen auf die nachfolgende Funktions- oder Klassendefinition.

Die nachfolgende Funktions- oder Klassendefinition ist immer benannt – der Name der nachfolgenden Definition wird dann verwendet, um die Vorwärtsreferenz aus der @in-Klausel zu erstellen.

Die @in-Klausel darf jede einfache Anweisung enthalten (einschließlich derer, die in diesem Kontext keinen Sinn ergeben, wie z. B. pass – obwohl ein solcher Code legal wäre, gäbe es keinen Sinn, ihn zu schreiben). Diese permissive Struktur ist einfacher zu definieren und zu erklären, aber ein restriktiverer Ansatz, der nur Operationen zulässt, die „Sinn ergeben“, wäre ebenfalls möglich (siehe PEP 3150 für eine Liste möglicher Kandidaten).

Die @in-Klausel erstellt keinen neuen Geltungsbereich – alle Namensbindungsoperationen mit Ausnahme der nachfolgenden Funktions- oder Klassendefinition wirken sich auf den umschließenden Geltungsbereich aus.

Der Name, der in der nachfolgenden Funktions- oder Klassendefinition verwendet wird, ist nur aus der zugehörigen @in-Klausel sichtbar und verhält sich so, als wäre er eine gewöhnliche Variable, die in diesem Geltungsbereich definiert wurde. Wenn in der @in-Klausel oder der nachfolgenden Funktions- oder Klassendefinition verschachtelte Geltungsbereiche erstellt werden, sehen diese Geltungsbereiche die nachfolgende Funktions- oder Klassendefinition anstelle anderer Bindungen für diesen Namen im umschließenden Geltungsbereich.

In einem sehr realen Sinne geht es bei diesem Vorschlag darum, die implizite Namensbindungsoperation „name = <definierte Funktion oder Klasse>“, die Teil jeder Funktions- oder Klassendefinition ist, überschreiben zu können, insbesondere in Fällen, in denen die lokale Namensbindung nicht wirklich benötigt wird.

Gemäß dieser PEP eine gewöhnliche Klassen- oder Funktionsdefinition

@deco2
@deco1
def name():
    ...

kann als ungefähr äquivalent zu folgendem erklärt werden

@in name = deco2(deco1(name))
def name():
    ...

Syntaxänderung

Syntaktisch ist nur eine neue Grammatikregel erforderlich

in_stmt: '@in' simple_stmt decorated

Grammatik: http://hg.python.org/cpython/file/default/Grammar/Grammar

Design-Diskussion

Hintergrund

Die Frage der „mehrzeiligen Lambdas“ ist für viele Python-Benutzer seit sehr langer Zeit ein kniffliges Problem, und es erforderte die Untersuchung der Blockfunktionalität von Ruby, damit ich endlich verstehe, warum dies die Leute so stört: Pythons Forderung, dass die Funktion benannt und vor der Operation, die sie benötigt, eingeführt wird, unterbricht den Gedankenfluss des Entwicklers. Sie kommen an einen Punkt, an dem sie denken: „Ich brauche eine Einmaloperation, die <X> tut“, und anstatt dies einfach direkt sagen zu können, müssen sie zurückgehen, eine Funktion benennen, die <X> tut, und dann diese Funktion von der Operation aus aufrufen, die sie ursprünglich tun wollten. Lambda-Ausdrücke können manchmal helfen, aber sie sind kein Ersatz dafür, eine vollständige Suite verwenden zu können.

Die Block-Syntax von Ruby inspirierte auch stark den Stil der Lösung in dieser PEP, indem sie klarstellte, dass selbst wenn sie auf *eine* anonyme Funktion pro Anweisung beschränkt sind, anonyme Funktionen immer noch unglaublich nützlich sein könnten. Betrachten Sie, wie viele Konstrukte Python hat, bei denen ein Ausdruck die Hauptlast der Arbeit trägt

  • Comprehensions, Generator-Ausdrücke, map(), filter()
  • Schlüsselargumente für sorted(), min(), max()
  • partielle Funktionsanwendung
  • Bereitstellung von Callbacks (z. B. für Weak References oder asynchrones IO)
  • Array-Broadcast-Operationen in NumPy

Die direkte Übernahme der Ruby-Block-Syntax funktioniert jedoch nicht für Python, da die Wirksamkeit von Ruby-Blöcken stark von verschiedenen Konventionen in der Art und Weise abhängt, wie Funktionen *definiert* werden (insbesondere die Verwendung von Rubys yield-Syntax zum direkten Aufrufen von Blöcken und der &arg-Mechanismus zum Akzeptieren eines Blocks als letztes Argument einer Funktion).

Da Python seit langem auf benannte Funktionen angewiesen ist, sind die Signaturen von APIs, die Callbacks akzeptieren, weitaus vielfältiger, sodass eine Lösung erforderlich ist, die es ermöglicht, Einmalfunktionen an der entsprechenden Stelle einzufügen.

Der in dieser PEP verfolgte Ansatz besteht darin, die Anforderung, die Funktion explizit zu benennen, beizubehalten, aber die relative Reihenfolge der Definition und der Anweisung, auf die sie verweist, zu ändern, um sie an den Gedankenfluss des Entwicklers anzupassen. Die Begründung ist im Wesentlichen dieselbe wie bei der Einführung von Decorators, deckt aber einen breiteren Bereich von Anwendungen ab.

Beziehung zu PEP 3150

PEP 3150 (Statement Local Namespaces) beschreibt seine Hauptmotivation darin, gewöhnliche Zuweisungsanweisungen auf eine Stufe mit class- und def-Anweisungen zu stellen, bei denen der Name des zu definierenden Elements dem Leser vor den Details der Berechnung dieses Elements präsentiert wird. Diese PEP erreicht dasselbe Ziel auf andere Weise, indem sie die einfache Namensbindung einer Standardfunktionsdefinition durch etwas anderes (wie die Zuweisung des Funktionsergebnisses zu einem Wert) ersetzt.

Trotz des gleichen Autors konkurrieren die beiden PEPs direkt miteinander. PEP 403 stellt einen minimalistischen Ansatz dar, der versucht, mit minimaler Änderung des Status quo nützliche Funktionalität zu erzielen. Diese PEP zielt stattdessen auf ein flexibleres, eigenständiges Anweisungsdesign ab, das einen größeren Eingriff in die Sprache erfordert.

Beachten Sie, dass PEP 403 besser geeignet ist, das Verhalten von Generator-Ausdrücken korrekt zu erklären, während diese PEP das Verhalten von Decorator-Klauseln im Allgemeinen besser erklären kann. Beide PEPs unterstützen ausreichende Erklärungen für die Semantik von Container-Comprehensions.

Auswahl des Schlüsselworts

Der Vorschlag erfordert definitiv *eine Art* Präfix, um Parsing-Mehrdeutigkeiten und Kompatibilitätsprobleme mit bestehenden Konstrukten zu vermeiden. Er muss auch für die Leser klar hervorgehoben werden, da er erklärt, dass der folgende Code erst ausgeführt wird, nachdem die nachfolgende Funktions- oder Klassendefinition ausgeführt wurde.

Das Schlüsselwort in wurde als bereits vorhandenes Schlüsselwort gewählt, das zur Bezeichnung des Konzepts einer Vorwärtsreferenz verwendet werden kann.

Das Präfix @ wurde aufgenommen, um die Tatsache auszunutzen, dass Python-Programmierer bereits an die Decorator-Syntax gewöhnt sind, als Hinweis auf eine nicht-sequenzielle Ausführung, bei der die Funktion oder Klasse tatsächlich *zuerst* definiert und dann Decorators in umgekehrter Reihenfolge angewendet werden.

Für Funktionen soll das Konstrukt gelesen werden als „in <dieser Anweisung, die NAME referenziert> definiere NAME als eine Funktion, die <Operation> tut“.

Die Abbildung auf englische Prosa ist für den Fall der Klassendefinition nicht so offensichtlich, aber das Konzept bleibt dasselbe.

Bessere Debugging-Unterstützung für Funktionen und Klassen mit kurzen Namen

Eine der Einwände gegen die weit verbreitete Verwendung von Lambda-Ausdrücken ist, dass sie sich negativ auf die Klarheit von Tracebacks und andere Aspekte der Introspektion auswirken. Ähnliche Einwände werden bezüglich Konstrukten erhoben, die kurze, kryptische Funktionsnamen fördern (einschließlich dieses, das erfordert, dass der Name der nachfolgenden Definition mindestens zweimal angegeben wird, was die Verwendung von Kurzschreib-Platzhalternamen wie f fördert).

Die Einführung qualifizierter Namen in PEP 3155 bedeutet jedoch, dass selbst anonyme Klassen und Funktionen nun unterschiedliche Darstellungen haben werden, wenn sie in verschiedenen Geltungsbereichen auftreten. Zum Beispiel

>>> def f():
...     return lambda: y
...
>>> f()
<function f.<locals>.<lambda> at 0x7f6f46faeae0>

Anonyme Funktionen (oder Funktionen, die sich einen Namen teilen) innerhalb desselben Geltungsbereichs werden immer noch Darstellungen teilen (abgesehen von der Objekt-ID), aber dies ist immer noch eine deutliche Verbesserung gegenüber der historischen Situation, in der alles *außer* der Objekt-ID identisch war.

Mögliche Implementierungsstrategie

Dieser Vorschlag hat mindestens einen riesigen Vorteil gegenüber PEP 3150: Die Implementierung sollte relativ unkompliziert sein.

Die @in-Klausel wird in die AST für die zugehörige Funktions- oder Klassendefinition und die Anweisung, auf die sie verweist, aufgenommen. Wenn die @in-Klausel vorhanden ist, wird sie anstelle der lokalen Namensbindungsoperation, die normalerweise durch eine Funktions- oder Klassendefinition impliziert wird, ausgegeben.

Der potenziell knifflige Teil ist die Änderung der Bedeutung der Referenzen auf die lokale Funktions- oder Namensraum-Anweisung innerhalb des Geltungsbereichs der in-Anweisung, aber das sollte durch die Beibehaltung von zusätzlichem Zustand im Compiler nicht allzu schwer zu lösen sein (es ist viel einfacher, dies für einen einzelnen Namen zu behandeln als für eine unbekannte Anzahl von Namen in einer vollständigen verschachtelten Suite).

Erklärung von Container-Comprehensions und Generator-Ausdrücken

Eine interessante Eigenschaft des vorgeschlagenen Konstrukts ist, dass es als Primitiv verwendet werden kann, um die Geltungsbereichs- und Ausführungsreihenfolgesemantik sowohl von Generator-Ausdrücken als auch von Container-Comprehensions zu erklären

seq2 = [x for x in y if q(x) for y in seq if p(y)]

# would be equivalent to

@in seq2 = f(seq):
def f(seq)
    result = []
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    result.append(x)
    return result

Der wichtige Punkt bei dieser Erweiterung ist, dass sie erklärt, warum Comprehensions im Klassen-Geltungsbereich fehlerhaft zu sein scheinen: Nur der äußerste Iterator wird im Klassen-Geltungsbereich ausgewertet, während alle Prädikate, verschachtelten Iteratoren und Wertausdrücke innerhalb eines verschachtelten Geltungsbereichs ausgewertet werden.

Eine äquivalente Erweiterung ist für Generator-Ausdrücke möglich

gen = (x for x in y if q(x) for y in seq if p(y))

# would be equivalent to

@in gen = g(seq):
def g(seq)
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    yield x

Weitere Beispiele

Berechnung von Attributen, ohne den lokalen Namensraum zu verschmutzen (aus os.py)

# Current Python (manual namespace cleanup)
def _createenviron():
    ... # 27 line function

environ = _createenviron()
del _createenviron

# Becomes:
@in environ = _createenviron()
def _createenviron():
    ... # 27 line function

Schleifen-Frühbindung

# Current Python (default argument hack)
funcs = [(lambda x, i=i: x + i) for i in range(10)]

# Becomes:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
    return lambda x: x + i

# Or even:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
    @in return incr
    def incr(x):
        return x + i

Eine nachgestellte Klasse kann als lokaler Namensraum für Anweisungen verwendet werden

# Evaluate subexpressions only once
@in c = math.sqrt(x.a*x.a + x.b*x.b)
class x:
    a = calculate_a()
    b = calculate_b()

Eine Funktion kann direkt an eine Stelle gebunden werden, die kein gültiger Bezeichner ist

@in dispatch[MyClass] = f
def f():
    ...

Konstrukte, die an der Grenze zum Missbrauch von Decorators liegen, können eliminiert werden

# Current Python
@call
def f():
    ...

# Becomes:
@in f()
def f():
    ...

Referenzimplementierung

Bisher keine.

Danksagungen

Vielen Dank an Gary Bernhardt, der unverblümt darauf hingewiesen hat, dass ich keine Ahnung hatte, wovon ich sprach, als ich die Ruby-Blöcke kritisierte, was einen ziemlich aufschlussreichen Untersuchungsprozess auslöste.

Abgelehnte Konzepte

Um nicht bereits abgedecktes Terrain neu zu behandeln, werden in diesem Abschnitt einige abgelehnte Alternativen dokumentiert.

Weglassen des Decorator-Präfixzeichens

Frühere Versionen dieses Vorschlags ließen das @-Präfix weg. Ohne dieses Präfix assoziierte das bloße Schlüsselwort in die Klausel nicht stark genug mit der nachfolgenden Funktions- oder Klassendefinition. Die Wiederverwendung des Decorator-Präfixes und die explizite Charakterisierung des neuen Konstrukts als eine Art Decorator-Klausel soll Benutzern helfen, die beiden Konzepte zu verknüpfen und sie als zwei Varianten derselben Idee zu sehen.

Anonyme Vorwärtsreferenzen

Eine frühere Inkarnation dieser PEP (siehe [1]) schlug eine Syntax vor, bei der die neue Klausel mit : eingeführt wurde und die Vorwärtsreferenz mit @ geschrieben wurde. Das Feedback zu dieser Variante war fast einstimmig negativ, da sie sowohl als hässlich als auch als übermäßig magisch empfunden wurde.

:x = weakref.ref(target, @)
def report_destruction(obj):
    print("{} is being destroyed".format(obj))

Eine neuere Variante verwendete für Vorwärtsreferenzen immer ... zusammen mit tatsächlich anonymen Funktions- und Klassendefinitionen. Dies degenerierte jedoch in komplexeren Fällen schnell zu einem Massiv unverständlicher Punkte.

in funcs = [...(i) for i in range(10)]
def ...(i):
  in return ...
  def ...(x):
      return x + i

in c = math.sqrt(....a*....a + ....b*....b)
class ...:
  a = calculate_a()
  b = calculate_b()

Verwendung einer verschachtelten Suite

Die Probleme bei der Verwendung einer vollständigen verschachtelten Suite werden am besten in PEP 3150 beschrieben. Sie ist vergleichsweise schwierig richtig zu implementieren, die Geltungsbereichssemantik ist schwerer zu erklären und sie schafft recht viele Situationen, in denen es zwei Möglichkeiten gibt, etwas zu tun, ohne klare Richtlinien für die Wahl dazwischen (da fast jedes Konstrukt, das mit gewöhnlichem imperativen Code ausgedrückt werden kann, stattdessen mit einer gegebenen Anweisung ausgedrückt werden könnte). Obwohl die PEP einige neue PEP 8-Richtlinien vorschlägt, um letzteres Problem anzugehen, sind die Schwierigkeiten bei der Implementierung nicht so leicht zu bewältigen.

Im Gegensatz dazu beschränkt die von Decorators inspirierte Syntax in dieser PEP die neue Funktion explizit auf Fälle, in denen sie die Lesbarkeit tatsächlich verbessern sollte, anstatt sie zu verschlechtern. Wie bei der ursprünglichen Einführung von Decorators ist die Idee dieser neuen Syntax, dass, wenn sie *verwendet werden kann* (d. h. die lokale Namensbindung der Funktion ist vollständig unnötig), sie wahrscheinlich *verwendet werden sollte*.

Eine weitere mögliche Variante dieser Idee ist, die Decorator-basierten *Semantiken* dieser PEP beizubehalten, während die schönere Syntax aus PEP 3150 übernommen wird.

x = weakref.ref(target, report_destruction) given:
    def report_destruction(obj):
        print("{} is being destroyed".format(obj))

Dieser Ansatz hat ein paar Probleme. Das Hauptproblem ist, dass diese Syntaxvariante etwas verwendet, das wie eine Suite aussieht, aber keine ist. Ein sekundäres Anliegen ist, dass nicht klar ist, wie der Compiler wissen wird, welche Namen im führenden Ausdruck Vorwärtsreferenzen sind (obwohl das potenziell durch eine geeignete Definition der Suite-die-keine-Suite-ist in der Sprachgrammatik gelöst werden könnte).

Eine verschachtelte Suite wurde jedoch noch nicht vollständig ausgeschlossen. Die neueste Version von PEP 3150 verwendet explizite Vorwärtsreferenz- und Namensbindungsmechanismen, die die Semantik der Anweisung stark vereinfachen, und sie bietet den Vorteil, die Definition beliebiger Unterausdrücke zu ermöglichen, anstatt auf eine einzelne Funktions- oder Klassendefinition beschränkt zu sein.

Referenzen


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT