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

Python Enhancement Proposals

PEP 3150 – Anweisung lokaler Namensräume (auch „given“-Klausel)

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Verschoben
Typ:
Standards Track
Erstellt:
09-Jul-2010
Python-Version:
3.4
Post-History:
14-Jul-2010, 21-Apr-2011, 13-Jun-2011

Inhaltsverzeichnis

Zusammenfassung

Dieses PEP schlägt die Hinzufügung einer optionalen given-Klausel zu mehreren Python-Anweisungen vor, die derzeit keine zugehörige Code-Suite haben. Diese Klausel erstellt einen anweisungs-lokalen Namensraum für zusätzliche Namen, die in der zugehörigen Anweisung zugänglich sind, aber nicht Teil des enthaltenden Namensraums werden.

Die Übernahme eines neuen Symbols, ?, wird vorgeschlagen, um eine Vorwärtsreferenz auf den Namensraum zu bezeichnen, der durch die Ausführung der zugehörigen Code-Suite erstellt wird. Es wird eine Referenz auf ein types.SimpleNamespace-Objekt sein.

Die Hauptmotivation ist, einen deklarativeren Programmierstil zu ermöglichen, bei dem die auszuführende Operation dem Leser zuerst präsentiert wird und die Details der notwendigen Unterberechnungen in der folgenden eingerückten Suite dargestellt werden. Als wichtiges Beispiel würde dies gewöhnliche Zuweisungsanweisungen auf die gleiche Stufe wie class- und def-Anweisungen heben, bei denen der Name des zu definierenden Elements dem Leser vor den Details der Berechnung dieses Elements präsentiert wird. Es erlaubt auch benannte Funktionen in einer „Multi-Line-Lambda“-Art und Weise zu verwenden, wobei der Name nur als Platzhalter im aktuellen Ausdruck verwendet und dann in der folgenden Suite definiert wird.

Eine sekundäre Motivation ist die Vereinfachung von Zwischenberechnungen in Modul- und Klassencode, ohne die resultierenden Namensräume zu verunreinigen.

Die Absicht ist, dass die Beziehung zwischen einer gegebenen Klausel und einer separaten Funktionsdefinition, die die angegebene Operation ausführt, ähnlich der bestehenden Beziehung zwischen einer expliziten while-Schleife und einem Generator ist, der die gleiche Sequenz von Operationen wie diese while-Schleife erzeugt.

Der spezifische Vorschlag in diesem PEP wurde durch verschiedene Erkundungen dieser und verwandter Konzepte im Laufe der Jahre beeinflusst (z. B. [1], [2], [3], [6], [8]) und ist bis zu einem gewissen Grad von den where- und let-Klauseln in Haskell inspiriert. Er vermeidet einige Probleme, die in früheren Vorschlägen identifiziert wurden, musste sich aber selbst noch nicht dem Test der Implementierung stellen.

Vorschlag

Dieses PEP schlägt die Hinzufügung einer optionalen given-Klausel zur Syntax für einfache Anweisungen vor, die einen Ausdruck enthalten können, oder diese Anweisung aus rein syntaktischen Gründen ersetzen können. Die aktuelle Liste der einfachen Anweisungen, die von dieser Ergänzung betroffen wären, ist wie folgt:

  • Ausdrucksanweisung
  • Zuweisungsanweisung
  • Erweiterte Zuweisungsanweisung
  • del-Anweisung
  • return-Anweisung
  • yield-Anweisung
  • raise-Anweisung
  • assert-Anweisung
  • pass-Anweisung

Die given-Klausel würde es ermöglichen, Unterausdrücke mit Namen in der Kopfzeile zu referenzieren, wobei die tatsächlichen Definitionen in der eingerückten Klausel folgen. Als einfaches Beispiel:

sorted_data = sorted(data, key=?.sort_key) given:
    def sort_key(item):
        return item.attr1, item.attr2

Das neue Symbol ? wird verwendet, um auf den gegebenen Namensraum zu verweisen. Es wäre eine types.SimpleNamespace-Instanz, also fungiert ?.sort_key als Vorwärtsreferenz auf einen Namen, der in der given-Klausel definiert ist.

Ein Docstring wäre in der given-Klausel zulässig und würde als __doc__-Attribut an den Ergebnisnamensraum angehängt werden.

Die pass-Anweisung ist enthalten, um eine konsistente Möglichkeit zu bieten, das Fehlen eines aussagekräftigen Ausdrucks in der Kopfzeile zu überspringen. Dies ist zwar kein beabsichtigter Anwendungsfall, aber auch keiner, der verhindert werden kann, da mehrere Alternativen (wie ... und ()) auch dann verfügbar bleiben, wenn pass selbst nicht zulässig ist.

Der Körper der given-Klausel wird in einem neuen Gültigkeitsbereich ausgeführt, unter Verwendung normaler Funktionsschluss-Semantik. Um die frühe Bindung von Schleifenvariablen und globalen Referenzen zu unterstützen, sowie um den Zugriff auf andere Namen auf Klassenebene zu ermöglichen, wird die given-Klausel auch explizite Bindungsoperationen in der Kopfzeile zulassen:

# Explicit early binding via given clause
seq = []
for i in range(10):
    seq.append(?.f) given i=i in:
        def f():
            return i
assert [f() for f in seq] == list(range(10))

Semantik

Die folgende Anweisung:

op(?.f, ?.g) given bound_a=a, bound_b=b in:
    def f():
        return bound_a + bound_b
    def g():
        return bound_a - bound_b

Wäre ungefähr äquivalent zum folgenden Code (__var bezeichnet eine versteckte Compiler-Variable oder einfach einen Eintrag auf dem Interpreter-Stack)

__arg1 = a
__arg2 = b
def __scope(bound_a, bound_b):
    def f():
        return bound_a + bound_b
    def g():
        return bound_a - bound_b
   return types.SimpleNamespace(**locals())
__ref = __scope(__arg1, __arg2)
__ref.__doc__ = __scope.__doc__
op(__ref.f, __ref.g)

Eine given-Klausel ist im Wesentlichen eine verschachtelte Funktion, die erstellt und dann sofort ausgeführt wird. Sofern nicht explizit übergeben, werden Namen mit normalen Gültigkeitsregeln nachgeschlagen, und somit sind Namen, die auf Klassenebene definiert sind, nicht sichtbar. Namen, die als Vorwärtsreferenzen deklariert sind, werden zurückgegeben und in der Kopfzeilenanweisung verwendet, ohne lokal im umgebenden Namensraum gebunden zu sein.

Syntaxänderung

Aktuell

expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
             ('=' (yield_expr|testlist_star_expr))*)
del_stmt: 'del' exprlist
pass_stmt: 'pass'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
assert_stmt: 'assert' test [',' test]

Neu

expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
             ('=' (yield_expr|testlist_star_expr))*) [given_clause]
del_stmt: 'del' exprlist [given_clause]
pass_stmt: 'pass' [given_clause]
return_stmt: 'return' [testlist] [given_clause]
yield_stmt: yield_expr [given_clause]
raise_stmt: 'raise' [test ['from' test]] [given_clause]
assert_stmt: 'assert' test [',' test] [given_clause]
given_clause: "given" [(NAME '=' test)+ "in"]":" suite

(Beachten Sie, dass expr_stmt in der Grammatik eine leichte Fehlbezeichnung ist, da sie Zuweisungen und erweiterte Zuweisungen zusätzlich zu einfachen Ausdrucksanweisungen abdeckt)

Hinweis

Diese vorgeschlagenen Grammatikänderungen decken die Syntax für Vorwärtsreferenzen noch nicht ab, um auf Namen zuzugreifen, die im anweisungs-lokalen Namensraum definiert sind.

Die neue Klausel wird als optionales Element zu den bestehenden Anweisungen hinzugefügt, anstatt als neue Art von zusammengesetzter Anweisung, um eine Mehrdeutigkeit in der Grammatik zu vermeiden. Sie wird nur auf die spezifisch aufgeführten Elemente angewendet, um Unsinn wie den folgenden zu verhindern:

break given:
    a = b = 1

import sys given:
    a = b = 1

Die genaue obige Grammatikänderung ist jedoch unzureichend, da sie Probleme für die Definition von simple_stmt verursacht (die das Verketten mehrerer einzeiliger Anweisungen mit „;“ anstelle von „\n“ zulässt).

Daher sollte die obige Syntaxänderung stattdessen als Absichtserklärung betrachtet werden. Jeder tatsächliche Vorschlag müsste das Parsing-Problem von simple_stmt lösen, bevor er ernsthaft in Betracht gezogen werden kann. Dies würde wahrscheinlich eine nicht unerhebliche Umstrukturierung der Grammatik erfordern, wobei small_stmt und flow_stmt aufgeteilt werden müssten, um die Anweisungen zu trennen, die potenziell beliebige Unterausdrücke enthalten können, und dann einer dieser Anweisungen mit einer given-Klausel auf der simple_stmt-Ebene erlaubt zu werden. Etwas in der Art von:

stmt: simple_stmt | given_stmt | compound_stmt
simple_stmt: small_stmt (';' (small_stmt | subexpr_stmt))* [';'] NEWLINE
small_stmt: (pass_stmt | flow_stmt | import_stmt |
             global_stmt | nonlocal_stmt)
flow_stmt: break_stmt | continue_stmt
given_stmt: subexpr_stmt (given_clause |
              (';' (small_stmt | subexpr_stmt))* [';']) NEWLINE
subexpr_stmt: expr_stmt | del_stmt | flow_subexpr_stmt | assert_stmt
flow_subexpr_stmt: return_stmt | raise_stmt | yield_stmt
given_clause: "given" (NAME '=' test)* ":" suite

Zur Referenz hier die aktuellen Definitionen auf dieser Ebene:

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt

Zusätzlich zu den oben genannten Änderungen würde die Definition von atom so geändert, dass sie auch ? zulässt. Die Einschränkung dieser Verwendung auf Anweisungen mit einer zugehörigen given-Klausel würde in einer späteren Phase des Kompilierungsprozesses behandelt werden (wahrscheinlich der AST-Konstruktion, die bereits andere Einschränkungen durchsetzt, wo die Grammatik zu nachgiebig ist, um den anfänglichen Parsing-Schritt zu vereinfachen).

Neue PEP 8 Richtlinien

Wie auf python-ideas diskutiert ([7], [9]), müssten auch neue PEP 8-Richtlinien entwickelt werden, um angemessene Anleitungen zu geben, wann die given-Klausel gegenüber gewöhnlichen Variablenszuweisungen zu verwenden ist.

Basierend auf ähnlichen Richtlinien, die bereits für try-Anweisungen vorhanden sind, schlägt dieses PEP die folgenden Ergänzungen für given-Anweisungen zum Abschnitt „Programmierkonventionen“ von PEP 8 vor:

  • Erwägen Sie für Code, der sinnvoll in eine separate Funktion ausgelagert werden könnte, aber derzeit nirgends wiederverwendet wird, die Verwendung einer given-Klausel. Dies zeigt klar an, welche Variablen nur zur Definition von Unterkomponenten einer anderen Anweisung verwendet werden und nicht zur Speicherung von Algorithmus- oder Anwendungszuständen. Dies ist eine besonders nützliche Technik, wenn Mehrzeilenfunktionen an Operationen übergeben werden, die aufrufbare Argumente erwarten.
  • Halten Sie given-Klauseln kurz. Wenn sie unhandlich werden, brechen Sie sie entweder in mehrere Schritte auf oder lagern Sie die Details in eine separate Funktion aus.

Begründung

Funktions- und Klassenanweisungen in Python haben eine einzigartige Eigenschaft im Vergleich zu gewöhnlichen Zuweisungsanweisungen: bis zu einem gewissen Grad sind sie *deklarativ*. Sie präsentieren dem Leser des Codes wichtige Informationen über einen Namen, der gerade definiert wird, bevor er mit den Details der eigentlichen Definition im Funktions- oder Klassenrumpf fortfährt.

Der *Name* des zu deklarierenden Objekts ist das erste, was nach dem Schlüsselwort angegeben wird. Andere wichtige Informationen erhalten ebenfalls die Ehre, den Implementierungsdetails voraus zu gehen:

  • Dekoratoren (die das Verhalten des erstellten Objekts stark beeinflussen können und aus praktischen, nicht aus ästhetischen Gründen sogar vor dem Schlüsselwort und dem Namen platziert wurden)
  • Der Docstring (in der ersten Zeile unmittelbar nach der Kopfzeile)
  • Parameter, Standardwerte und Annotationen für Funktionsdefinitionen
  • Oberklassen, Metaklasse und optional andere Details (abhängig von der Metaklasse) für Klassendefinitionen

Dieses PEP schlägt vor, einen ähnlichen deklarativen Stil für beliebige Zuweisungsoperationen verfügbar zu machen, indem eine „given“-Suite nach jeder einfachen Zuweisungsanweisung zugelassen wird.

TARGET = [TARGET2 = ... TARGETN =] EXPR given:
    SUITE

Konventionell sollte sich der Code im Rumpf der Suite ausschließlich auf die korrekte Definition der Zuweisungsoperation in der Kopfzeile konzentrieren. Die Operation in der Kopfzeile sollte ebenfalls ausreichend beschreibend sein (z. B. durch geeignete Namenswahl für Variablen), um dem Leser eine angemessene Vorstellung vom Zweck der Operation zu vermitteln, ohne den Rumpf der Suite lesen zu müssen.

Obwohl sie der anfänglich motivierende Anwendungsfall sind, wäre es übermäßig einschränkend, dieses Feature nur auf einfache Zuweisungen zu beschränken. Sobald das Feature überhaupt definiert ist, wäre es ziemlich willkürlich, seine Verwendung für erweiterte Zuweisungen, Return-Anweisungen, Yield-Ausdrücke, Comprehensions und beliebige Ausdrücke, die den Anwendungszustand modifizieren können, zu verbieten.

Die given-Klausel kann auch als lesbarere Alternative zu einigen Verwendungen von Lambda-Ausdrücken und ähnlichen Konstrukten dienen, wenn Einwegfunktionen an Operationen wie sorted() übergeben werden oder in Callback-basierter ereignisgesteuerter Programmierung.

In Modul- und Klassenebene dient die given-Klausel als klarer und zuverlässiger Ersatz für die Verwendung der del-Anweisung, um Zwischenvariablen von der Verunreinigung des resultierenden Namensraums abzuhalten.

Eine potenziell nützliche Art, die vorgeschlagene Klausel zu betrachten, ist als Mittelweg zwischen konventionellem Inline-Code und der Auslagerung einer Operation in eine dedizierte Funktion, so wie eine Inline-While-Schleife schließlich in einen dedizierten Generator ausgelagert werden kann.

Design-Diskussion

Schlüsselwortwahl

Dieser Vorschlag verwendete ursprünglich where, basierend auf dem Namen eines ähnlichen Konstrukts in Haskell. Es wurde jedoch darauf hingewiesen, dass es bereits bestehende Python-Bibliotheken gibt (wie Numpy [4]), die where bereits im Sinne von SQL-Abfragebedingungen verwenden, was diese Schlüsselwortwahl potenziell verwirrend macht.

Obwohl given auch als Variablenname verwendet werden kann (und somit durch den üblichen __future__-Dance zur Einführung neuer Schlüsselwörter als veraltet gelten würde), ist es viel stärker mit der gewünschten Semantik „Hier sind einige zusätzliche Variablen, die dieser Ausdruck verwenden darf“ für die neue Klausel verbunden.

Die Wiederverwendung des Schlüsselworts with wurde ebenfalls vorgeschlagen. Dies hat den Vorteil, die Hinzufügung eines neuen Schlüsselworts zu vermeiden, birgt aber auch ein hohes Verwirrungspotenzial, da die with-Klausel und die with-Anweisung ähnlich aussehen, aber völlig unterschiedliche Dinge tun. Das ist der Weg zu C++ und Perl :)

Beziehung zu PEP 403

PEP 403 (General Purpose Decorator Clause) versucht, die Hauptziele dieses PEPs mit einer weniger radikalen Sprachänderung zu erreichen, die von der bestehenden Decorator-Syntax inspiriert ist.

Trotz des gleichen Autors stehen die beiden PEPs in direktem Wettbewerb zueinander. PEP 403 stellt einen minimalistischen Ansatz dar, der versucht, nützliche Funktionalität mit minimaler Änderung des Status quo zu erreichen. Dieses PEP zielt stattdessen auf ein flexibleres, eigenständiges Anweisungsdesign ab, das einen größeren Wandel in der Sprache erfordert.

Beachten Sie, dass PEP 403 besser geeignet ist, das Verhalten von Generator Expressions korrekt zu erklären, während dieses PEP besser in der Lage ist, das Verhalten von Decorator-Klauseln im Allgemeinen zu erklären. Beide PEPs unterstützen angemessene Erklärungen für die Semantik von Container Comprehensions.

Erläuterung von Container Comprehensions und Generator Expressions

Ein interessantes Merkmal des vorgeschlagenen Konstrukts ist, dass es als Primitiv verwendet werden kann, um die Gültigkeitsbereiche und die Ausführungsreihenfolge-Semantik 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

seq2 = ?.result given seq=seq:
    result = []
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    result.append(x)

Der wichtige Punkt in dieser Erweiterung ist, dass sie erklärt, warum Comprehensions im Klassengültigkeitsbereich fehlerhaft erscheinen: nur der äußerste Iterator wird im Klassengültigkeitsbereich ausgewertet, während alle Prädikate, verschachtelten Iteratoren und Werteausdrücke innerhalb eines verschachtelten Gültigkeitsbereichs ausgewertet werden.

Nicht, dass im Gegensatz zu PEP 403 die aktuelle Version dieses PEP *keine* exakt äquivalente Erweiterung für eine Generator Expression liefern *kann*. Das Nächstliegende, was es erreichen kann, ist die Definition einer zusätzlichen Ebene der Gültigkeit:

seq2 = ?.g(seq) given:
    def g(seq):
        for y in seq:
            if p(y):
                for x in y:
                    if q(x):
                        yield x

Diese Einschränkung könnte behoben werden, indem die given-Klausel eine Generatorfunktion erlaubt, in diesem Fall würde ? sich auf ein Generator-Iterator-Objekt und nicht auf einen einfachen Namensraum beziehen.

seq2 = ? given seq=seq in:
    for y in seq:
        if p(y):
            for x in y:
                if q(x):
                    yield x

Dies würde jedoch die Bedeutung von „?“ ziemlich mehrdeutig machen, sogar noch mehr als bereits der Fall für die Bedeutung von def-Anweisungen (die normalerweise einen Docstring haben werden, der angibt, ob eine Funktionsdefinition tatsächlich ein Generator ist).

Erläuterung der Auswertung und Anwendung von Decorator-Klauseln

Die Standarderklärung der Auswertung und Anwendung von Decorator-Klauseln muss die Idee versteckter Compiler-Variablen behandeln, um Schritte in ihrer Ausführungsreihenfolge anzuzeigen. Die given-Anweisung erlaubt eine dekorierte Funktionsdefinition wie:

@classmethod
def classname(cls):
    return cls.__name__

Um stattdessen ungefähr äquivalent zu sein zu:

classname = .d1(classname) given:
    d1 = classmethod
    def classname(cls):
        return cls.__name__

Erwartete Einwände

Zwei Wege, es zu tun

Viele Codezeilen können nun mit Werten geschrieben werden, die entweder vor dem Ausdruck, in dem sie verwendet werden, oder danach in einer given-Klausel definiert sind, wodurch zwei Wege entstehen, es zu tun, vielleicht ohne eine offensichtliche Möglichkeit, zwischen ihnen zu wählen.

Bei genauerer Betrachtung halte ich dies für eine Fehlinterpretation des Aphorismus „ein offensichtlicher Weg“. Python bietet bereits *viele* Wege, Code zu schreiben. Wir können eine for-Schleife oder eine while-Schleife, einen funktionalen Stil oder einen imperativen Stil oder einen objektorientierten Stil verwenden. Die Sprache ist im Allgemeinen so konzipiert, dass sie es den Leuten erlaubt, Code so zu schreiben, wie sie denken. Da unterschiedliche Leute unterschiedlich denken, wird sich die Art und Weise, wie sie ihren Code schreiben, entsprechend ändern.

Solche stilistischen Fragen in einer Codebasis sind zu Recht der Entwicklungsgruppe überlassen, die für diesen Code verantwortlich ist. Wann wird ein Ausdruck so kompliziert, dass die Unterausdrücke herausgenommen und Variablen zugewiesen werden sollten, obwohl diese Variablen nur einmal verwendet werden? Wann sollte eine Inline-While-Schleife durch einen Generator ersetzt werden, der die gleiche Logik implementiert? Die Meinungen gehen auseinander, und das ist in Ordnung.

Allerdings werden explizite PEP 8-Richtlinien für CPython und die Standardbibliothek benötigt, und das wird im obigen Vorschlag diskutiert.

Ausführung außerhalb der Reihenfolge

Die given-Klausel lässt die Ausführung ein wenig seltsam springen, da der Rumpf der given-Klausel vor der einfachen Anweisung im Klauselkopf ausgeführt wird. Das Nächstliegende, was irgendein anderer Teil von Python dem nahekommt, ist die Auswertung außerhalb der Reihenfolge in Listen-Comprehensions, Generator Expressions und bedingten Ausdrücken sowie die verzögerte Anwendung von Decorator-Funktionen auf die Funktion, die sie dekorieren (die Decorator-Ausdrücke selbst werden in der Reihenfolge ausgeführt, in der sie geschrieben werden).

Obwohl dies wahr ist, ist die Syntax für Fälle gedacht, in denen Leute selbst *außerhalb der Reihenfolge* über ein Problem *nachdenken* (zumindest in Bezug auf die Sprache). Als Beispiel hierfür betrachten Sie den folgenden Gedanken eines Python-Benutzers:

Ich möchte die Elemente dieser Sequenz nach den Werten von attr1 und attr2 jedes Elements sortieren.

Wenn sie mit Pythons lambda-Ausdrücken vertraut sind, könnten sie es so schreiben:

sorted_list = sorted(original, key=(lambda v: v.attr1, v.attr2))

Das erledigt die Aufgabe, erreicht aber kaum den Standard von ausführbarem Pseudocode, der Pythons Ruf entspricht.

Wenn sie lambda speziell nicht mögen, bietet das operator-Modul eine Alternative, die es immer noch erlaubt, die Schlüssel-Funktion inline zu definieren:

sorted_list = sorted(original,
                     key=operator.attrgetter(v. 'attr1', 'attr2'))

Auch dies erledigt die Aufgabe, aber selbst der großzügigste Leser würde dies nicht als „ausführbaren Pseudocode“ bezeichnen.

Wenn sie beide oben genannten Optionen als hässlich und verwirrend empfinden oder Logik in ihrer Schlüssel-Funktion benötigen, die nicht als Ausdruck ausgedrückt werden kann (wie das Abfangen einer Ausnahme), zwingt Python sie derzeit, die Reihenfolge ihres ursprünglichen Gedankens umzukehren und zuerst das Sortierkriterium zu definieren:

def sort_key(item):
    return item.attr1, item.attr2

sorted_list = sorted(original, key=sort_key)

„Definiere einfach eine Funktion“ ist seit Jahren die Standardantwort auf Anfragen nach Unterstützung für mehrzeilige Lambdas. Wie bei den obigen Optionen erledigt dies die Aufgabe, stellt aber wirklich einen Bruch dar zwischen dem, was der Benutzer denkt, und dem, was die Sprache ihm ausdrücken lässt.

Ich glaube, der Vorschlag in diesem PEP würde Python endlich in die Nähe der „ausführbaren Pseudocode“-Marke für die Art von Gedanken bringen, die oben ausgedrückt wurde.

sorted_list = sorted(original, key=?.key) given:
    def key(item):
        return item.attr1, item.attr2

Alles ist in der gleichen Reihenfolge wie im ursprünglichen Gedanken des Benutzers, und er muss nicht einmal einen Namen für das Sortierkriterium erfinden: Es ist möglich, den Namen des Schlüsselwortarguments direkt wiederzuverwenden.

Eine mögliche Verbesserung dieser Vorschläge wäre die Bereitstellung einer praktischen Kurzschreibweise, um „verwende den Inhalt der given-Klausel als Schlüsselwortargumente“ zu sagen. Selbst ohne dedizierte Syntax kann dies einfach als **vars(?) geschrieben werden.

Schädlich für Introspektion

Das Stochern in Modul- und Klasseninterne ist ein unschätzbares Werkzeug für White-Box-Tests und interaktives Debugging. Die given-Klausel wird recht effektiv darin sein, den Zugriff auf temporären Zustand, der während Berechnungen verwendet wird, zu verhindern (obwohl nicht mehr als die aktuelle Verwendung von del-Anweisungen in dieser Hinsicht).

Obwohl dies eine berechtigte Sorge ist, ist das Design für Testbarkeit ein Thema, das viele Aspekte der Programmierung betrifft. Wenn eine Komponente unabhängig getestet werden muss, sollte eine given-Anweisung in separate Anweisungen refaktorisiert werden, damit Informationen der Testsuite zugänglich gemacht werden. Dies unterscheidet sich nicht wesentlich von der Refaktorierung einer Operation, die in einer Funktion oder einem Generator verborgen ist, in ihre eigene Funktion, nur um sie isoliert testen zu können.

Mangelnde Bewertung realer Auswirkungen

Die Beispiele im aktuellen PEP sind fast ausschließlich relativ kleine „Spielzeug“-Beispiele. Der Vorschlag in diesem PEP muss dem Test der Anwendung auf eine große Codebasis (wie die Standardbibliothek oder eine große Twisted-Anwendung) unterzogen werden, um Beispiele zu finden, bei denen die Lesbarkeit realer Codebasis wirklich verbessert wird.

Dies ist jedoch eher eine Schwäche des PEPs als der Idee. Wenn es kein reales Problem wäre, würden wir nicht so viele Beschwerden über das Fehlen von Mehrzeilen-Lambda-Unterstützung bekommen und die Block-Konstruktion von Ruby wäre wahrscheinlich nicht so beliebt.

Offene Fragen

Syntax für Vorwärtsreferenzen

Das Symbol ? wird für Vorwärtsreferenzen zum given-Namensraum vorgeschlagen, da es kurz ist, derzeit ungenutzt ist und „hier fehlt etwas, das später aufgefüllt wird“ suggeriert.

Der Vorschlag im PEP weist keine klare Parallele zu einer bestehenden Python-Funktion auf, daher wurde die Wiederverwendung eines bereits verwendeten Symbols bewusst vermieden.

Behandlung von nonlocal und global

nonlocal und global sind in der given-Klausel-Suite explizit nicht zulässig und führen zu Syntaxfehlern, wenn sie auftreten. Sie funktionieren normal, wenn sie innerhalb einer def-Anweisung innerhalb dieser Suite auftreten.

Alternativ könnten sie so definiert werden, dass sie so funktionieren, als wären die anonymen Funktionen wie in der obigen Erweiterung definiert worden.

Behandlung von break und continue

break und continue verhalten sich so, als wären die anonymen Funktionen wie in der obigen Erweiterung definiert worden. Sie führen zu Syntaxfehlern, wenn sie in der given-Klausel-Suite auftreten, funktionieren aber normal, wenn sie innerhalb einer for- oder while-Schleife als Teil dieser Suite auftreten.

Behandlung von return und yield

return und yield sind in der given-Klausel-Suite explizit nicht zulässig und führen zu Syntaxfehlern, wenn sie auftreten. Sie funktionieren normal, wenn sie innerhalb einer def-Anweisung innerhalb dieser Suite auftreten.

Beispiele

Definition von Callbacks für ereignisgesteuerte Programmierung

# Current Python (definition before use)
def cb(sock):
    # Do something with socket
def eb(exc):
    logging.exception(
        "Failed connecting to %s:%s", host, port)
loop.create_connection((host, port), cb, eb) given:

# Becomes:
loop.create_connection((host, port), ?.cb, ?.eb) given:
    def cb(sock):
        # Do something with socket
    def eb(exc):
        logging.exception(
            "Failed connecting to %s:%s", host, port)

Definition von „Einmaligen“ Klassen, die typischerweise nur eine einzige Instanz haben

# Current Python (instantiation after definition)
class public_name():
  ... # However many lines
public_name = public_name(*params)

# Current Python (custom decorator)
def singleton(*args, **kwds):
    def decorator(cls):
        return cls(*args, **kwds)
    return decorator

@singleton(*params)
class public_name():
  ... # However many lines

# Becomes:
public_name = ?.MeaningfulClassName(*params) given:
  class MeaningfulClassName():
    ... # Should trawl the stdlib for an example of doing this

Berechnung von Attributen ohne Verunreinigung des lokalen Namensraums (aus os.py)

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

environ = _createenviron()
del _createenviron

# Becomes:
environ = ?._createenviron() given:
    def _createenviron():
      ... # 27 line function

Ersetzen des Standardargument-Hacks (aus functools.lru_cache)

# Current Python (default argument hack)
def decorating_function(user_function,
               tuple=tuple, sorted=sorted, len=len, KeyError=KeyError):
  ... # 60 line function
return decorating_function

# Becomes:
return ?.decorating_function given:
  # Cell variables rather than locals, but should give similar speedup
  tuple, sorted, len, KeyError = tuple, sorted, len, KeyError
  def decorating_function(user_function):
    ... # 60 line function

# This example also nicely makes it clear that there is nothing in the
# function after the nested function definition. Due to additional
# nested functions, that isn't entirely clear in the current code.

Mögliche Ergänzungen

  • Der aktuelle Vorschlag erlaubt die Hinzufügung einer given-Klausel nur für einfache Anweisungen. Die Ausweitung der Idee auf die Verwendung von zusammengesetzten Anweisungen wäre durchaus möglich (durch Anhängen der given-Klausel als eigenständige Suite am Ende), aber dies birgt ernsthafte Lesbarkeitsprobleme (da Werte, die in der given-Klausel definiert sind, lange vor ihrer Definition verwendet werden können, genau die Art von Lesbarkeitsfalle, die andere Features wie Decorators und with-Anweisungen zu vermeiden suchen).
  • Die Variante „explizite frühe Bindung“ könnte für die Diskussionen auf python-ideas relevant sein, wie der Standardargument-Hack eliminiert werden kann. Eine given-Klausel in der Kopfzeile für Funktionen (nach der Rückgabetypanmerkung) könnte die Antwort auf diese Frage sein.

Abgelehnte Alternativen

  • Eine frühere Version dieses PEPs erlaubte implizite Vorwärtsreferenzen auf die Namen in der nachfolgenden Suite und verwendete auch implizite frühe Bindungssemantik. Beide Ideen verkomplizierten den Vorschlag erheblich, ohne eine ausreichende Steigerung der Ausdrucksstärke zu bieten. Der aktuelle Vorschlag mit expliziten Vorwärtsreferenzen und früher Bindung bringt das neue Konstrukt in Einklang mit bestehenden Gültigkeitssemantiken, was die Chancen auf eine tatsächliche Implementierung erheblich verbessert.
  • Zusätzlich zu den hier gemachten Vorschlägen gab es auch Vorschläge für „In-Order“-Varianten mit zwei Suiten, die die eingeschränkte Gültigkeit von Namen bieten, ohne eine Ausführung außerhalb der Reihenfolge zu unterstützen. Ich glaube, diese Vorschläge verfehlen weitgehend den Punkt dessen, worüber sich die Leute beschweren, wenn sie nach Unterstützung für mehrzeilige Lambdas fragen – es ist nicht so, dass das Finden eines Namens für den Unterausdruck besonders schwierig ist, sondern dass die Benennung der Funktion vor der Anweisung, die sie verwendet, bedeutet, dass der Code nicht mehr mit der Art und Weise übereinstimmt, wie der Entwickler das Problem betrachtet.
  • Ich habe einige unveröffentlichte Versuche unternommen, direkte Referenzen auf die Closure zu ermöglichen, die implizit durch die given-Klausel erstellt wird, während die allgemeine Struktur der Syntax wie in diesem PEP definiert bleibt (zum Beispiel, indem ein Unterausdruck wie ?given oder :given in Ausdrücken verwendet wird, um eine direkte Referenz auf die implizite Closure anzuzeigen und so zu verhindern, dass sie automatisch zur Erstellung des lokalen Namensraums aufgerufen wird). Alle solchen Versuche erschienen unattraktiv und verwirrend im Vergleich zu dem einfacheren, von Decorator inspirierten Vorschlag in PEP 403.

Referenzimplementierung

Noch keine. Wenn Sie einen Crashkurs in Python-Namensraum-Semantik und Codekompilierung wünschen, probieren Sie es gerne aus ;)

TO-DO

  • Erwähnen Sie PEP 359 und mögliche Verwendungen für locals() in der given-Klausel
  • Finden Sie heraus, ob dies intern verwendet werden kann, um die Implementierung von super()-Aufrufen ohne Argumente weniger schrecklich zu machen

Referenzen


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

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