PEP 671 – Syntax für spät gebundene Funktionsargument-Standardwerte
- Autor:
- Chris Angelico <rosuav at gmail.com>
- Discussions-To:
- Python-Ideas Thread
- Status:
- Entwurf
- Typ:
- Standards Track
- Erstellt:
- 24. Okt. 2021
- Python-Version:
- 3.12
- Post-History:
- 24. Okt. 2021, 01. Dez. 2021
Zusammenfassung
Funktionsparameter können Standardwerte haben, die während der Funktionsdefinition berechnet und gespeichert werden. Dieser Vorschlag führt eine neue Form von Argumentstandardwerten ein, die durch einen Ausdruck definiert wird, der zur Zeit des Funktionsaufrufs ausgewertet wird.
Motivation
Optionale Funktionsargumente haben, wenn sie weggelassen werden, oft eine logische Standardwert. Wenn dieser Wert von anderen Argumenten abhängt oder bei jedem Funktionsaufruf neu ausgewertet werden muss, gibt es derzeit keine saubere Möglichkeit, dies im Funktionsheader anzugeben.
Derzeit zulässige Idiome hierfür sind
# Very common: Use None and replace it in the function
def bisect_right(a, x, lo=0, hi=None, *, key=None):
if hi is None:
hi = len(a)
# Also well known: Use a unique custom sentinel object
_USE_GLOBAL_DEFAULT = object()
def connect(timeout=_USE_GLOBAL_DEFAULT):
if timeout is _USE_GLOBAL_DEFAULT:
timeout = default_timeout
# Unusual: Accept star-args and then validate
def add_item(item, *optional_target):
if not optional_target:
target = []
else:
target = optional_target[0]
In jeder Form schlägt help(function) fehl, den wahren Standardwert anzuzeigen. Jede hat auch zusätzliche Probleme; die Verwendung von None ist nur gültig, wenn None selbst kein plausibler Funktionsparameter ist, das benutzerdefinierte Sentinel erfordert eine globale Konstante; und die Verwendung von Star-Args impliziert, dass mehr als ein Argument übergeben werden könnte.
Spezifikation
Funktionsstandardargumente können mit der neuen =>-Notation definiert werden
def bisect_right(a, x, lo=0, hi=>len(a), *, key=None):
def connect(timeout=>default_timeout):
def add_item(item, target=>[]):
def format_time(fmt, time_t=>time.time()):
Der Ausdruck wird in seiner Quellcodeform zur Inspektion gespeichert, und Bytecode zur Auswertung wird vor den Funktionsrumpf gestellt.
Bemerkenswerterweise wird der Ausdruck im Laufzeitbereich der Funktion ausgewertet, NICHT im Bereich, in dem die Funktion definiert wurde (wie bei früh gebundenen Standardwerten). Dies ermöglicht es dem Ausdruck, auf andere Argumente zu verweisen.
Mehrere spät gebundene Argumente werden von links nach rechts ausgewertet und können auf zuvor definierte Werte verweisen. Die Reihenfolge wird von der Funktion bestimmt, unabhängig von der Reihenfolge, in der Schlüsselwortargumente übergeben werden.
def prevref(word=”foo”, a=>len(word), b=>a//2): # Gültig def selfref(spam=>spam): # UnboundLocalError def spaminate(sausage=>eggs + 1, eggs=>sausage - 1): # Verwirrend, mach das nicht def frob(n=>len(items), items=[]): # Siehe unten
Die Auswertungsreihenfolge ist von links nach rechts; Implementierungen KÖNNEN sich jedoch dafür entscheiden, dies in zwei separaten Durchläufen zu tun, zuerst für alle übergebenen Argumente und früh gebundenen Standardwerte, und dann einen zweiten Durchlauf für spät gebundene Standardwerte. Andernfalls werden alle Argumente streng von links nach rechts zugewiesen.
Abgelehnte Schreibweisen
Während dieses Dokument eine einzige Syntax name=>expression angibt, sind alternative Schreibweisen ähnlich plausibel. Die folgenden Schreibweisen wurden in Betracht gezogen
def bisect(a, hi=>len(a)):
def bisect(a, hi:=len(a)):
def bisect(a, hi?=len(a)):
def bisect(a, @hi=len(a)):
Da Standardargumente sich weitgehend gleich verhalten, ob sie früh oder spät gebunden sind, ist die gewählte Syntax hi=>len(a) bewusst ähnlich zur bestehenden Syntax für früh gebundene Argumente.
Ein Grund für die Ablehnung der :=-Syntax ist ihr Verhalten bei Annotationen. Annotationen gehen dem Standard voraus, daher muss in allen Syntaxoptionen eindeutig (sowohl für den Menschen als auch für den Parser) sein, ob es sich um eine Annotation, einen Standardwert oder beides handelt. Die alternative Syntax target:=expr birgt das Risiko, fälschlicherweise als target:int=expr missverstanden zu werden, wobei die Annotation irrtümlich weggelassen wird, und kann somit Fehler maskieren. Die gewählte Syntax target=>expr hat dieses Problem nicht.
Wie man das lehrt
Früh gebundene Standardargumente sollten immer zuerst gelehrt werden, da sie die einfachere und effizientere Methode zur Auswertung von Argumenten sind. Aufbauend auf ihnen sind spät gebundene Argumente weitgehend äquivalent zu Code am Anfang der Funktion.
def add_item(item, target=>[]):
# Equivalent pseudocode:
def add_item(item, target=<OPTIONAL>):
if target was omitted: target = []
Eine einfache Faustregel lautet: „target=expression“ wird ausgewertet, wenn die Funktion definiert wird, und „target=>expression“ wird ausgewertet, wenn die Funktion aufgerufen wird. In beiden Fällen, wenn das Argument zur Aufrufzeit bereitgestellt wird, wird der Standardwert ignoriert. Dies erklärt zwar nicht alle Feinheiten vollständig, ist aber ausreichend, um die wichtige Unterscheidung hier abzudecken (und die Tatsache, dass sie ähnlich sind).
Interaktion mit anderen Vorschlägen
PEP 661 versucht, eines der gleichen Probleme zu lösen wie dieses. Es zielt darauf ab, die Dokumentation von Sentinel-Werten in Standardargumenten zu verbessern, während dieser Vorschlag die Notwendigkeit von Sentinels in vielen gängigen Fällen beseitigen möchte. PEP 661 kann die Dokumentation in beliebig komplizierten Funktionen verbessern (er nennt traceback.print_exception als Hauptmotivation, die zwei Argumente hat, die beide oder keiner angegeben werden müssen); andererseits würden viele der gängigen Fälle keine Sentinels mehr benötigen, wenn der wahre Standardwert von der Funktion definiert werden könnte. Zusätzlich können dedizierte Sentinel-Objekte als Schlüssel für Wörterbuchsuchen verwendet werden, wo PEP 671 nicht gilt.
Ein generisches System für verzögerte Auswertung wurde zu verschiedenen Zeiten vorgeschlagen (nicht zu verwechseln mit PEP 563 und PEP 649, die sich speziell auf Annotationen beziehen). Obwohl es auf den ersten Blick so erscheinen mag, dass spät gebundene Argumentstandardwerte von ähnlicher Natur sind, sind sie tatsächlich unabhängig und orthogonal und beide könnten für die Sprache von Wert sein. Die Annahme oder Ablehnung dieses Vorschlags würde die Machbarkeit eines Vorschlags zur verzögerten Auswertung nicht beeinträchtigen und umgekehrt. (Ein wesentlicher Unterschied zwischen allgemeiner verzögerter Auswertung und Argumentstandardwerten besteht darin, dass Argumentstandardwerte immer und nur zu Beginn der Funktionsausführung ausgewertet werden, während verzögerte Ausdrücke erst bei Referenzierung realisiert würden.)
Implementierungsdetails
Das Folgende bezieht sich auf die Referenzimplementierung und ist nicht notwendigerweise Teil der Spezifikation.
Argumentstandardwerte (positional oder keyword) haben sowohl ihre Werte, wie bereits beibehalten, als auch ein zusätzliches Informationselement. Für Positionsargumente werden die Extras in einem Tupel in __defaults_extra__ gespeichert, und für keyword-only in einem Dictionary in __kwdefaults_extra__. Wenn dieses Attribut None ist, ist dies äquivalent dazu, None für jeden Argumentstandardwert zu haben.
Für jeden Parameter mit einem spät gebundenen Standardwert wird der spezielle Wert Ellipsis als Wertplatzhalter gespeichert, und die entsprechende zusätzliche Information muss abgefragt werden. Wenn sie None ist, ist der Standardwert tatsächlich der Wert Ellipsis; andernfalls ist es ein beschreibender String und der tatsächliche Wert wird berechnet, wenn die Funktion beginnt.
Wenn ein Parameter mit einem spät gebundenen Standardwert weggelassen wird, beginnt die Funktion mit dem ungebundenen Parameter. Die Funktion beginnt, indem sie jeden Parameter mit einem spät gebundenen Standardwert mithilfe eines neuen Opcodes QUERY_FAST/QUERY_DEREF prüft und, wenn ungebunden, den ursprünglichen Ausdruck auswertet. Dieser Opcode (nur für schnelle Locals und Closure-Variablen verfügbar) pusht True auf den Stack, wenn das gegebene lokale eine Wert hat, und False, wenn nicht – was bedeutet, dass er False pusht, wenn LOAD_FAST oder LOAD_DEREF eine UnboundLocalError auslösen würde, und True, wenn es erfolgreich wäre.
Aufeinanderfolgende Variablenreferenzen sind zulässig, solange der Referent einen Wert von einem Argument oder einem früh gebundenen Standardwert hat.
Kosten
Wenn keine spät gebundenen Argumentstandardwerte verwendet werden, sollten die folgenden Kosten die einzigen sein, die entstehen.
- Funktionsobjekte benötigen zwei zusätzliche Zeiger, die NULL sein werden
- Das Kompilieren von Code und das Erstellen von Funktionen beinhalten zusätzliche Flag-Prüfungen.
- Die Verwendung von
Ellipsisals Standardwert erfordert eine Laufzeitüberprüfung, um festzustellen, ob spät gebundene Standardwerte vorhanden sind.
Diese Kosten werden als minimal eingeschätzt (auf 64-Bit-Linux erhöht sich die Größe aller Funktionsobjekte von 152 auf 168 Bytes) mit praktisch keinen Laufzeitkosten, wenn spät gebundene Standardwerte nicht verwendet werden.
Rückwärtsinkompatibilität
Wo spät gebundene Standardwerte nicht verwendet werden, sollte das Verhalten identisch sein. Vorsicht ist geboten, wenn Ellipsis gefunden wird, da es sich möglicherweise nicht selbst repräsentiert, aber darüber hinaus sollten Tools den bestehenden Code unverändert sehen.
Referenzen
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0671.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT