PEP 340 – Anonyme Blockanweisungen
- Autor:
- Guido van Rossum
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 27-Apr-2005
- Post-History:
Inhaltsverzeichnis
- Einleitung
- Ablehnungsbescheid
- Motivation und Zusammenfassung
- Anwendungsfälle
- Spezifikation: die __exit__() Methode
- Spezifikation: die anonyme Blockanweisung
- Spezifikation: Generator-Exit-Behandlung
- Berücksichtigte und abgelehnte Alternativen
- Vergleich mit Thunks
- Beispiele
- Danksagungen
- Referenzen
- Urheberrecht
Einleitung
Diese PEP schlägt einen neuen Typ einer zusammengesetzten Anweisung vor, der für die Ressourcenverwaltung verwendet werden kann. Der neue Anweisungstyp wird vorläufig als Blockanweisung bezeichnet, da das zu verwendende Schlüsselwort noch nicht ausgewählt wurde.
Diese PEP konkurriert mit mehreren anderen PEPs: PEP 288 (Generator-Attribute und -Ausnahmen; nur der zweite Teil), PEP 310 (Zuverlässige Erfassungs-/Freigabepaare) und PEP 325 (Ressourcenfreigabestützung für Generatoren).
Ich sollte klarstellen, dass die Verwendung eines Generators zum „Steuern“ einer Blockanweisung ein wirklich separater Vorschlag ist; mit der reinen Definition der Blockanweisung aus der PEP könnten Sie alle Beispiele mit einer Klasse implementieren (ähnlich wie Beispiel 6, das leicht in eine Vorlage umgewandelt werden kann). Aber die Kernidee ist die Verwendung eines Generators zur Steuerung einer Blockanweisung; der Rest ist Ausarbeitung, daher möchte ich diese beiden Teile zusammenhalten.
(PEP 342, Erweiterte Iteratoren, war ursprünglich Teil dieser PEP; aber die beiden Vorschläge sind wirklich unabhängig und mit Stevens Bethards Hilfe habe ich ihn in eine separate PEP verschoben.)
Ablehnungsbescheid
Ich lehne diese PEP zugunsten von PEP 343 ab. Sehen Sie sich den Motivationsabschnitt in dieser PEP für die Begründung dieser Ablehnung an. GvR.
Motivation und Zusammenfassung
(Danke an Shane Hathaway – Hallo Shane!)
Gute Programmierer lagern häufig verwendete Codeblöcke in wiederverwendbare Funktionen aus. Manchmal treten Muster jedoch eher in der Struktur von Funktionen als in der tatsächlichen Anweisungssequenz auf. Viele Funktionen erwerben beispielsweise eine Sperre, führen einen für die Funktion spezifischen Code aus und geben die Sperre bedingungslos frei. Das Wiederholen des Sperrcodes in jeder Funktion, die ihn verwendet, ist fehleranfällig und erschwert das Refactoring.
Blockanweisungen bieten einen Mechanismus zur Kapselung von Strukturmustern. Code innerhalb einer Blockanweisung wird unter der Kontrolle eines Objekts namens Block-Iterator ausgeführt. Einfache Block-Iteratoren führen Code vor und nach dem Code innerhalb der Blockanweisung aus. Block-Iteratoren haben auch die Möglichkeit, den gesteuerten Code mehrmals auszuführen (oder gar nicht), Ausnahmen abzufangen oder Daten aus dem Körper der Blockanweisung zu empfangen.
Eine bequeme Möglichkeit, Block-Iteratoren zu schreiben, ist die Verwendung eines Generators (PEP 255). Ein Generator sieht einer Python-Funktion sehr ähnlich, aber anstatt sofort einen Wert zurückzugeben, pausieren Generatoren ihre Ausführung bei „yield“-Anweisungen. Wenn ein Generator als Block-Iterator verwendet wird, weist die yield-Anweisung den Python-Interpreter an, den Block-Iterator anzuhalten, den Körper der Blockanweisung auszuführen und den Block-Iterator fortzusetzen, wenn der Körper ausgeführt wurde.
Der Python-Interpreter verhält sich wie folgt, wenn er auf eine Blockanweisung stößt, die auf einem Generator basiert. Zuerst instanziiert der Interpreter den Generator und beginnt mit dessen Ausführung. Der Generator führt Setup-Arbeiten durch, die dem gekapselten Muster entsprechen, wie z. B. das Erwerben einer Sperre, das Öffnen einer Datei, das Starten einer Datenbanktransaktion oder das Starten einer Schleife. Dann gibt der Generator die Ausführung an den Körper der Blockanweisung ab und verwendet eine yield-Anweisung. Wenn der Körper der Blockanweisung abgeschlossen ist, eine unbehandelte Ausnahme auslöst oder Daten über eine continue-Anweisung an den Generator zurücksendet, wird der Generator fortgesetzt. An diesem Punkt kann der Generator entweder aufräumen und stoppen oder erneut yielden, wodurch der Körper der Blockanweisung erneut ausgeführt wird. Wenn der Generator beendet ist, verlässt der Interpreter die Blockanweisung.
Anwendungsfälle
Siehe den Abschnitt Beispiele am Ende.
Spezifikation: die __exit__() Methode
Eine optionale neue Methode für Iteratoren wird vorgeschlagen, genannt __exit__(). Sie nimmt bis zu drei Argumente entgegen, die den drei „Argumenten“ der raise-Anweisung entsprechen: Typ, Wert und Traceback. Wenn alle drei Argumente None sind, kann sys.exc_info() konsultiert werden, um geeignete Standardwerte zu liefern.
Spezifikation: die anonyme Blockanweisung
Eine neue Anweisung wird mit der folgenden Syntax vorgeschlagen:
block EXPR1 as VAR1:
BLOCK1
Hier sind „block“ und „as“ neue Schlüsselwörter; EXPR1 ist ein beliebiger Ausdruck (aber keine Ausdrucksliste) und VAR1 ist ein beliebiges Zuweisungsziel (das eine durch Kommas getrennte Liste sein kann).
Der Teil „as VAR1“ ist optional; wenn er weggelassen wird, werden die Zuweisungen an VAR1 in der folgenden Übersetzung weggelassen (aber die zugewiesenen Ausdrücke werden trotzdem ausgewertet!).
Die Wahl des Schlüsselworts „block“ ist umstritten; viele Alternativen wurden vorgeschlagen, einschließlich der Verwendung überhaupt kein Schlüsselwort (was ich eigentlich mag). PEP 310 verwendet „with“ für ähnliche Semantik, aber ich möchte dies für eine with-Anweisung reservieren, die der in Pascal und VB gefundenen ähnelt. (Obwohl ich gerade festgestellt habe, dass die C#-Designer „with“ nicht mögen [2], und ich stimme ihrer Begründung zu.) Um dieses Problem vorübergehend zu umgehen, verwende ich „block“, bis wir uns auf das richtige Schlüsselwort geeinigt haben, falls überhaupt.
Beachten Sie, dass das Schlüsselwort „as“ nicht umstritten ist (es wird schließlich zum Status eines echten Schlüsselworts erhoben).
Beachten Sie, dass es am Iterator liegt, zu entscheiden, ob eine Blockanweisung eine Schleife mit mehreren Iterationen darstellt; im häufigsten Anwendungsfall wird BLOCK1 genau einmal ausgeführt. Für den Parser ist es jedoch immer eine Schleife; break und continue kehren zum Iterator des Blocks zurück (siehe unten für Details).
Die Übersetzung unterscheidet sich subtil von einer for-Schleife: iter() wird nicht aufgerufen, daher sollte EXPR1 bereits ein Iterator sein (nicht nur ein iterierbares Objekt); und der Iterator erhält garantiert eine Benachrichtigung, wenn die Blockanweisung verlassen wird, unabhängig davon, ob dies aufgrund eines break, return oder einer Ausnahme geschieht.
itr = EXPR1 # The iterator
ret = False # True if a return statement is active
val = None # Return value, if ret == True
exc = None # sys.exc_info() tuple if an exception is active
while True:
try:
if exc:
ext = getattr(itr, "__exit__", None)
if ext is not None:
VAR1 = ext(*exc) # May re-raise *exc
else:
raise exc[0], exc[1], exc[2]
else:
VAR1 = itr.next() # May raise StopIteration
except StopIteration:
if ret:
return val
break
try:
ret = False
val = exc = None
BLOCK1
except:
exc = sys.exc_info()
(Die Variablen „itr“ usw. sind jedoch nicht für den Benutzer sichtbar und die verwendeten integrierten Namen können nicht vom Benutzer überschrieben werden.)
Innerhalb von BLOCK1 gelten die folgenden speziellen Übersetzungen
- „break“ ist immer legal; es wird übersetzt zu
exc = (StopIteration, None, None) continue
- „return EXPR3“ ist nur legal, wenn die Blockanweisung in einer Funktionsdefinition enthalten ist; sie wird übersetzt zu
exc = (StopIteration, None, None) ret = True val = EXPR3 continue
Der Nettoeffekt ist, dass break und return sich weitgehend wie bei einer for-Schleife verhalten, mit dem Unterschied, dass der Iterator die Möglichkeit zur Ressourcenbereinigung erhält, bevor die Blockanweisung verlassen wird, über die optionale __exit__()-Methode. Der Iterator erhält auch eine Chance, wenn die Blockanweisung durch Auslösen einer Ausnahme verlassen wird. Wenn der Iterator keine __exit__()-Methode hat, gibt es keinen Unterschied zu einer for-Schleife (außer dass eine for-Schleife iter() auf EXPR1 aufruft).
Beachten Sie, dass eine yield-Anweisung in einer Blockanweisung nicht anders behandelt wird. Sie suspendiert die Funktion, die den Block enthält, **ohne** den Iterator des Blocks zu benachrichtigen. Der Iterator des Blocks ist sich dieses Yields vollständig unbewusst, da der lokale Kontrollfluss die Blockanweisung nicht tatsächlich verlässt. Mit anderen Worten, es ist **nicht** wie eine break- oder return-Anweisung. Wenn die durch das Yield wiederhergestellte Schleife next() aufruft, wird der Block direkt nach dem Yield fortgesetzt. (Siehe Beispiel 7 unten.) Die unten beschriebenen Generator-Finalisierungssemantik garantiert (innerhalb der Grenzen aller Finalisierungssemantik), dass der Block irgendwann wiederhergestellt wird.
Im Gegensatz zur for-Schleife hat die Blockanweisung keine else-Klausel. Ich denke, das wäre verwirrend und würde die „Schleifigkeit“ der Blockanweisung hervorheben, während ich ihren **Unterschied** zu einer for-Schleife hervorheben möchte. Außerdem gibt es mehrere mögliche Semantiken für eine else-Klausel und nur einen sehr schwachen Anwendungsfall.
Spezifikation: Generator-Exit-Behandlung
Generatoren werden die neue __exit__()-Methoden-API implementieren.
Generatoren dürfen eine yield-Anweisung innerhalb einer try-finally-Anweisung haben.
Das Ausdrucksargument für die yield-Anweisung wird optional (standardmäßig None).
Wenn __exit__() aufgerufen wird, wird der Generator fortgesetzt, aber an der Stelle der yield-Anweisung wird die Ausnahme ausgelöst, die durch die __exit__-Argumente repräsentiert wird. Der Generator kann diese Ausnahme erneut auslösen, eine andere Ausnahme auslösen oder einen anderen Wert yielden, außer dass, wenn die an __exit__() übergebene Ausnahme StopIteration war, StopIteration ausgelöst werden sollte (andernfalls wäre der Effekt, dass ein break in continue umgewandelt wird, was zumindest unerwartet ist). Wenn der **erste** Aufruf, der den Generator fortsetzt, ein __exit__()-Aufruf anstelle eines next()-Aufrufs ist, wird die Ausführung des Generators abgebrochen und die Ausnahme erneut ausgelöst, ohne die Kontrolle an den Körper des Generators weiterzugeben.
Wenn ein Generator, der noch nicht beendet wurde, gesammelt wird (entweder durch Referenzzählung oder durch den zyklischen Garbage Collector), wird seine __exit__()-Methode einmal mit StopIteration als erstem Argument aufgerufen. Zusammen mit der Anforderung, dass ein Generator StopIteration auslösen sollte, wenn __exit__() mit StopIteration aufgerufen wird, garantiert dies die endgültige Aktivierung aller finally-Klauseln, die aktiv waren, als der Generator zuletzt suspendiert wurde. Natürlich kann der Generator unter bestimmten Umständen nie gesammelt werden. Dies unterscheidet sich nicht von den Garantien, die für Finalizer (__del__()-Methoden) anderer Objekte gemacht werden.
Berücksichtigte und abgelehnte Alternativen
- Viele Alternativen wurden für ‚block‘ vorgeschlagen. Ich habe noch keinen Vorschlag für ein anderes Schlüsselwort gesehen, das mir besser gefällt als ‚block‘. Leider ist ‚block‘ auch keine gute Wahl; es ist ein recht gebräuchlicher Name für Variablen, Argumente und Methoden. Vielleicht ist ‚with‘ doch die beste Wahl?
- Anstatt zu versuchen, das ideale Schlüsselwort auszuwählen, könnte die Blockanweisung einfach die Form haben
EXPR1 as VAR1: BLOCK1
Dies ist zunächst attraktiv, denn zusammen mit einer guten Wahl von Funktionsnamen (wie in den Beispielen unten) in
EXPR1liest es sich gut und fühlt sich wie eine „benutzerdefinierte Anweisung“ an. Und doch macht es mir (und vielen anderen) Unbehagen; ohne Schlüsselwort ist die Syntax sehr „fade“, schwer in einem Handbuch nachzuschlagen (denken Sie daran, dass ‚as‘ optional ist), und es macht die Bedeutung von break und continue in der Blockanweisung noch verwirrender. - Phillip Eby hat vorgeschlagen, dass die Blockanweisung eine völlig andere API als die for-Schleife verwendet, um zwischen den beiden zu unterscheiden. Ein Generator müsste in einen Decorator verpackt werden, um die Block-API zu unterstützen. Meiner Meinung nach fügt dies mehr Komplexität mit sehr wenig Nutzen hinzu; und wir können nicht wirklich leugnen, dass die Blockanweisung konzeptionell eine Schleife ist – sie unterstützt schließlich break und continue.
- Dies wird immer wieder vorgeschlagen: „block VAR1 = EXPR1“ anstelle von „block EXPR1 as VAR1“. Das wäre sehr irreführend, da VAR1 **nicht** den Wert von EXPR1 zugewiesen bekommt; EXPR1 ergibt einen Generator, der einer internen Variablen zugewiesen wird, und VAR1 ist der Wert, der von aufeinanderfolgenden Aufrufen der
__next__()-Methode dieses Iterators zurückgegeben wird. - Warum nicht die Übersetzung ändern, um
iter(EXPR1)anzuwenden? Alle Beispiele würden weiterhin funktionieren. Aber dies macht die Blockanweisung **mehr** wie eine for-Schleife, während der Schwerpunkt auf dem **Unterschied** zwischen den beiden liegen sollte. Das Nichtaufrufen voniter()fängt eine Reihe von Missverständnissen ab, wie z. B. die Verwendung einer Sequenz alsEXPR1.
Vergleich mit Thunks
Alternative Semantiken, die für die Blockanweisung vorgeschlagen wurden, verwandeln den Block in einen Thunk (eine anonyme Funktion, die in den enthaltenden Bereich verschmilzt).
Der Hauptvorteil von Thunks, den ich sehe, ist, dass man den Thunk für später speichern kann, wie einen Callback für ein Button-Widget (der Thunk wird dann zu einem Closure). Dies kann man nicht mit einem yield-basierten Block tun (außer in Ruby, das die yield-Syntax mit einer Thunk-basierten Implementierung verwendet). Aber ich muss sagen, dass ich dies fast als Vorteil sehe: Ich glaube, ich wäre leicht unbehaglich, einen Block zu sehen und nicht zu wissen, ob er im normalen Kontrollfluss oder später ausgeführt wird. Die Definition einer expliziten verschachtelten Funktion für diesen Zweck hat dieses Problem für mich nicht, da ich bereits weiß, dass das Schlüsselwort ‚def‘ bedeutet, dass ihr Körper später ausgeführt wird.
Das andere Problem mit Thunks ist, dass, sobald wir sie als die anonymen Funktionen betrachten, die sie sind, wir ziemlich sicher sagen müssen, dass ein return-Statement in einem Thunk aus dem Thunk und nicht aus der enthaltenden Funktion zurückkehrt. Jede andere Vorgehensweise würde zu größeren Komplikationen führen, wenn der Thunk seine enthaltende Funktion als Closure überlebt (vielleicht könnten Fortsetzungen helfen, aber ich werde mich nicht damit beschäftigen :-)).
Aber dann geht ein meiner Meinung nach wichtiger Anwendungsfall für das Ressourcenbereinigungs-Template-Muster verloren. Ich schreibe routinemäßig Code wie diesen
def findSomething(self, key, default=None):
self.lock.acquire()
try:
for item in self.elements:
if item.matches(key):
return item
return default
finally:
self.lock.release()
und ich wäre enttäuscht, wenn ich das nicht so schreiben könnte
def findSomething(self, key, default=None):
block locking(self.lock):
for item in self.elements:
if item.matches(key):
return item
return default
Dieses spezielle Beispiel kann mit einem break neu geschrieben werden
def findSomething(self, key, default=None):
block locking(self.lock):
for item in self.elements:
if item.matches(key):
break
else:
item = default
return item
aber es wirkt erzwungen und die Umwandlung ist nicht immer so einfach; man wäre gezwungen, seinen Code in einem Single-Return-Stil neu zu schreiben, was zu einschränkend ist.
Beachten Sie auch das semantische Dilemma eines Yields in einem Thunk – die einzig vernünftige Interpretation ist, dass dies den Thunk zu einem Generator macht!
Greg Ewing glaubt, dass Thunks „viel einfacher wären, genau das tun, was erforderlich ist, ohne irgendeinen Hokuspokus mit Ausnahmen und break/continue/return-Anweisungen. Es wäre einfach zu erklären, was sie tun und warum sie nützlich sind.“
Aber um die erforderliche lokale Variablenfreigabe zwischen dem Thunk und der enthaltenden Funktion zu erreichen, müsste jede lokale Variable, die im Thunk verwendet oder gesetzt wird, zu einer ‚Zelle‘ werden (unser Mechanismus zur Freigabe von Variablen zwischen verschachtelten Geltungsbereichen). Zellen verlangsamen den Zugriff im Vergleich zu regulären lokalen Variablen: Der Zugriff beinhaltet einen zusätzlichen C-Funktionsaufruf (PyCell_Get() oder PyCell_Set()).
Vielleicht nicht ganz zufällig zeigt das letzte Beispiel oben (findSomething(), neu geschrieben, um einen return innerhalb des Blocks zu vermeiden), dass im Gegensatz zu regulären verschachtelten Funktionen Variablen, die vom Thunk **zugewiesen** werden, auch mit der enthaltenden Funktion geteilt werden sollen, selbst wenn sie außerhalb des Thunks nicht zugewiesen werden.
Greg Ewing wieder: „Generatoren haben sich als leistungsfähiger erwiesen, da man mehrere davon gleichzeitig im Einsatz haben kann. Gibt es dafür einen Nutzen?“
Ich glaube, es gibt definitiv Nutzen dafür; mehrere Leute haben bereits gezeigt, wie man asynchrone, leichtgewichtige Threads mit Generatoren macht (z. B. David Mertz, zitiert in PEP 288, und Fredrik Lundh [3]).
Und schließlich sagt Greg: „Eine Thunk-Implementierung hat das Potenzial, mehrere Blockargumente leicht zu handhaben, wenn eine geeignete Syntax jemals entwickelt werden könnte. Es ist schwer zu sehen, wie das in einem allgemeinen Fall mit der Generatorimplementierung geschehen könnte.“
Die Anwendungsfälle für mehrere Blöcke scheinen jedoch schwer fassbar zu sein.
(Später wurden Vorschläge gemacht, die Implementierung von Thunks zu ändern, um die meisten dieser Einwände zu beseitigen, aber die resultierende Semantik ist ziemlich komplex zu erklären und zu implementieren, so dass dies meiner Meinung nach den Zweck der Verwendung von Thunks von vornherein verfehlt.)
Beispiele
(Mehrere dieser Beispiele enthalten „yield None“. Wenn PEP 342 akzeptiert wird, können diese natürlich zu „yield“ geändert werden.)
- Eine Vorlage, um sicherzustellen, dass eine Sperre, die zu Beginn eines Blocks erworben wurde, freigegeben wird, wenn der Block verlassen wird
def locking(lock): lock.acquire() try: yield None finally: lock.release()
Verwendet wie folgt
block locking(myLock): # Code here executes with myLock held. The lock is # guaranteed to be released when the block is left (even # if via return or by an uncaught exception).
- Eine Vorlage zum Öffnen einer Datei, die sicherstellt, dass die Datei geschlossen wird, wenn der Block verlassen wird
def opening(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
Verwendet wie folgt
block opening("/etc/passwd") as f: for line in f: print line.rstrip()
- Eine Vorlage zum Committen oder Rollback einer Datenbanktransaktion
def transactional(db): try: yield None except: db.rollback() raise else: db.commit()
- Eine Vorlage, die etwas bis zu n Mal versucht
def auto_retry(n=3, exc=Exception): for i in range(n): try: yield None return except exc, err: # perhaps log exception here continue raise # re-raise the exception we caught earlier
Verwendet wie folgt
block auto_retry(3, IOError): f = urllib.urlopen("https://www.example.com/") print f.read()
- Es ist möglich, Blöcke zu verschachteln und Vorlagen zu kombinieren
def locking_opening(lock, filename, mode="r"): block locking(lock): block opening(filename) as f: yield f
Verwendet wie folgt
block locking_opening(myLock, "/etc/passwd") as f: for line in f: print line.rstrip()
(Wenn dieses Beispiel Sie verwirrt, bedenken Sie, dass es äquivalent zur Verwendung einer for-Schleife mit einem Yield in ihrem Körper in einem regulären Generator ist, der einen anderen Iterator oder Generator rekursiv aufruft; siehe zum Beispiel den Quellcode für
os.walk().) - Es ist möglich, einen regulären Iterator mit der Semantik von Beispiel 1 zu schreiben
class locking: def __init__(self, lock): self.lock = lock self.state = 0 def __next__(self, arg=None): # ignores arg if self.state: assert self.state == 1 self.lock.release() self.state += 1 raise StopIteration else: self.lock.acquire() self.state += 1 return None def __exit__(self, type, value=None, traceback=None): assert self.state in (0, 1, 2) if self.state == 1: self.lock.release() raise type, value, traceback
(Dieses Beispiel kann leicht modifiziert werden, um die anderen Beispiele zu implementieren; es zeigt, wie viel einfacher Generatoren für denselben Zweck sind.)
- Stdout temporär umleiten
def redirecting_stdout(new_stdout): save_stdout = sys.stdout try: sys.stdout = new_stdout yield None finally: sys.stdout = save_stdout
Verwendet wie folgt
block opening(filename, "w") as f: block redirecting_stdout(f): print "Hello world"
- Eine Variante von
opening(), die auch eine Fehlerbedingung zurückgibtdef opening_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
Verwendet wie folgt
block opening_w_error("/etc/passwd", "a") as f, err: if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
Danksagungen
In keiner bestimmten Reihenfolge: Alex Martelli, Barry Warsaw, Bob Ippolito, Brett Cannon, Brian Sabbey, Chris Ryland, Doug Landauer, Duncan Booth, Fredrik Lundh, Greg Ewing, Holger Krekel, Jason Diamond, Jim Jewett, Josiah Carlson, Ka-Ping Yee, Michael Chermside, Michael Hudson, Neil Schemenauer, Alyssa Coghlan, Paul Moore, Phillip Eby, Raymond Hettinger, Georg Brandl, Samuele Pedroni, Shannon Behrens, Skip Montanaro, Steven Bethard, Terry Reedy, Tim Delaney, Aahz und andere. Danke an alle für die wertvollen Beiträge!
Referenzen
[1] https://mail.python.org/pipermail/python-dev/2005-April/052821.html
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0340.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT