PEP 346 – Benutzerdefinierte ("with")-Anweisungen
- Autor:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Status:
- Zurückgezogen
- Typ:
- Standards Track
- Erstellt:
- 06-Mai-2005
- Python-Version:
- 2.5
- Post-History:
Inhaltsverzeichnis
- Zusammenfassung
- Hinweis des Autors
- Einleitung
- Beziehung zu anderen PEPs
- Benutzerdefinierte Anweisungen
- Generatoren
- Standardwert für
yield - Template-Generator-Decorator:
statement_template - Template-Generator-Wrapper:
__enter__()-Methode - Template-Generator-Wrapper:
__exit__()-Methode - Ausnahmen in Generatoren injizieren
- Generator-Finalisierung
- Generator-Finalisierung:
TerminateIteration-Ausnahme - Generator-Finalisierung:
__del__()-Methode - Deterministische Generator-Finalisierung
- Generatoren als benutzerdefinierte Statement-Templates
- Standardwert für
- Beispiele
- Offene Fragen
- Abgelehnte Optionen
- Die grundlegende Konstruktion als Schleifenkonstrukt zu haben
- Statement-Templates zuzulassen, Ausnahmen zu unterdrücken
- Unterscheidung zwischen nicht-ausnahmebedingten Beendigungen
- Ausgelöste Ausnahmen nicht in Generatoren zu injizieren
- Alle Generatoren zu Statement-Templates zu machen
- Das Schlüsselwort
dozu verwenden - Kein Schlüsselwort zu haben
- Verbesserung von
try-Anweisungen - Das Template-Protokoll direkt
try-Anweisungen widerspiegeln zu lassen
- Iterator-Finalisierung (ZURÜCKGEZOGEN)
- Danksagungen
- Referenzen
- Urheberrecht
Zusammenfassung
Dieser PEP ist eine Kombination aus PEP 310s "Reliable Acquisition/Release Pairs" mit den "Anonymous Block Statements" von Guidos PEP 340. Dieser PEP zielt darauf ab, die guten Teile von PEP 340 zu nehmen, sie mit Teilen von PEP 310 zu vermischen und das Ganze neu zu arrangieren, um ein elegantes Ganzes zu schaffen. Er leiht sich von verschiedenen anderen PEPs, um ein vollständiges Bild zu malen, und ist dazu bestimmt, für sich allein zu stehen.
Einleitung
Dieser PEP schlägt vor, die Fähigkeit von Python, Ressourcen zuverlässig zu verwalten, durch die Einführung einer neuen with-Anweisung zu verbessern, die die Faktorisierung von beliebigem try/finally und einiger try/except/else Boilerplate ermöglicht. Das neue Konstrukt wird als "benutzerdefinierte Anweisung" bezeichnet, und die zugehörigen Klassendefinitionen als "Statement-Templates".
Das Obige ist der Hauptpunkt des PEP. Wenn dies jedoch alles wäre, was er sagt, dann wäre PEP 310 ausreichend und dieser PEP im Wesentlichen redundant. Stattdessen empfiehlt dieser PEP zusätzliche Erweiterungen, die es natürlich machen, diese Statement-Templates mit entsprechend dekorierten Generatoren zu schreiben. Eine Nebenwirkung dieser Erweiterungen ist, dass es wichtig wird, die Verwaltung von Ressourcen innerhalb von Generatoren angemessen zu handhaben.
Dies ist PEP 343 sehr ähnlich, aber die auftretenden Ausnahmen werden im Frame des Generators erneut ausgelöst, und die Frage der Generator-Finalisierung muss infolgedessen behandelt werden. Der von diesem PEP vorgeschlagene Template-Generator-Decorator erstellt außerdem wiederverwendbare Templates, im Gegensatz zu den einzeln verwendbaren Templates von PEP 340.
Im Vergleich zu PEP 340 eliminiert dieser PEP die Möglichkeit, Ausnahmen zu unterdrücken, und macht die benutzerdefinierte Anweisung zu einem nicht-iterativen Konstrukt. Der andere Hauptunterschied ist die Verwendung eines Decorators, um Generatoren in Statement-Templates umzuwandeln, und die Einbeziehung von Ideen zur Behandlung der Iterator-Finalisierung.
Wenn all das wie eine ehrgeizige Operation erscheint... nun, Guido hat die Messlatte so hoch gelegt, als er PEP 340 schrieb :)
Beziehung zu anderen PEPs
Dieser PEP konkurriert direkt mit PEP 310, PEP 340 und PEP 343, da diese PEPs alle alternative Mechanismen für die Behandlung der deterministischen Ressourcenverwaltung beschreiben.
Er konkurriert nicht mit PEP 342, der PEP 340s Erweiterungen im Zusammenhang mit der Übergabe von Daten an Iteratoren abspaltet. Die damit verbundenen Änderungen an der for-Schleifen-Semantik würden mit den in diesem PEP vorgeschlagenen Iterator-Finalisierungsänderungen kombiniert. Benutzerdefinierte Anweisungen wären nicht betroffen.
Ebenso konkurriert dieser PEP nicht mit den in PEP 288 beschriebenen Generatorerweiterungen. Während dieser PEP die Möglichkeit vorschlägt, Ausnahmen in Generator-Frames zu injizieren, handelt es sich um ein internes Implementierungsdetail und erfordert nicht, diese Möglichkeit dem Python-Code öffentlich zugänglich zu machen. PEP 288 befasst sich teilweise damit, dieses Implementierungsdetail leicht zugänglich zu machen.
Dieser PEP würde jedoch die in PEP 325 beschriebene Generator-Ressourcenfreigabe-Unterstützung überflüssig machen – Iteratoren, die eine Finalisierung benötigen, sollten eine angemessene Implementierung des Statement-Template-Protokolls bereitstellen.
Benutzerdefinierte Anweisungen
Um das motivierende Beispiel aus PEP 310 zu stehlen: Die korrekte Handhabung eines Synchronisationsschlosses sieht derzeit so aus
the_lock.acquire()
try:
# Code here executes with the lock held
finally:
the_lock.release()
Wie PEP 310 schlägt dieser PEP vor, dass solcher Code geschrieben werden kann als
with the_lock:
# Code here executes with the lock held
Diese benutzerdefinierten Anweisungen sind in erster Linie dazu gedacht, die einfache Faktorisierung von try-Blöcken zu ermöglichen, die nicht leicht in Funktionen umgewandelt werden können. Dies ist am häufigsten der Fall, wenn das Muster der Ausnahmebehandlung konsistent ist, aber der Körper des try-Blocks sich ändert. Mit einer benutzerdefinierten Anweisung ist es einfach, die Ausnahmebehandlung in ein Statement-Template zu faktorisieren, wobei der Körper der try-Klausel inline im Benutzercode bereitgestellt wird.
Der Begriff "benutzerdefinierte Anweisung" spiegelt die Tatsache wider, dass die Bedeutung einer with-Anweisung hauptsächlich durch das verwendete Statement-Template bestimmt wird, und Programmierer frei sind, ihre eigenen Statement-Templates zu erstellen, so wie sie frei sind, ihre eigenen Iteratoren für die Verwendung in for-Schleifen zu erstellen.
Verwendungssyntax für benutzerdefinierte Anweisungen
Die vorgeschlagene Syntax ist einfach
with EXPR1 [as VAR1]:
BLOCK1
Semantik für benutzerdefinierte Anweisungen
the_stmt = EXPR1
stmt_enter = getattr(the_stmt, "__enter__", None)
stmt_exit = getattr(the_stmt, "__exit__", None)
if stmt_enter is None or stmt_exit is None:
raise TypeError("Statement template required")
VAR1 = stmt_enter() # Omit 'VAR1 =' if no 'as' clause
exc = (None, None, None)
try:
try:
BLOCK1
except:
exc = sys.exc_info()
raise
finally:
stmt_exit(*exc)
Abgesehen von VAR1 werden keine der oben gezeigten lokalen Variablen vom Benutzercode aus sichtbar sein. Wie die Iterationsvariable in einer for-Schleife ist VAR1 sowohl in BLOCK1 als auch im Code nach der benutzerdefinierten Anweisung sichtbar.
Beachten Sie, dass das Statement-Template nur auf Ausnahmen reagieren, sie aber nicht unterdrücken kann. Siehe Abgelehnte Optionen für eine Erklärung, warum.
Statement-Template-Protokoll: __enter__
Die __enter__()-Methode nimmt keine Argumente entgegen, und wenn sie eine Ausnahme auslöst, wird BLOCK1 nie ausgeführt. Wenn dies geschieht, wird die __exit__()-Methode nicht aufgerufen. Der von dieser Methode zurückgegebene Wert wird VAR1 zugewiesen, wenn die as-Klausel verwendet wird. Objekte, die keinen anderen Wert zurückgeben sollen, sollten im Allgemeinen self anstelle von None zurückgeben, um die Erstellung "in-place" in der with-Anweisung zu ermöglichen.
Statement-Templates sollten diese Methode verwenden, um die Bedingungen einzurichten, die während der Ausführung der Anweisung bestehen sollen (z. B. Erwerb eines Synchronisationsschlosses).
Statement-Templates, die nicht immer verwendbar sind (z. B. geschlossene Dateiobjekte), sollten eine RuntimeError auslösen, wenn versucht wird, __enter__() aufzurufen, wenn das Template nicht in einem gültigen Zustand ist.
Statement-Template-Protokoll: __exit__
Die __exit__()-Methode akzeptiert drei Argumente, die den drei "Argumenten" der raise-Anweisung entsprechen: Typ, Wert und Traceback. Alle Argumente werden immer übergeben und auf None gesetzt, wenn keine Ausnahme aufgetreten ist. Diese Methode wird genau einmal von der with-Anweisungsmaschinerie aufgerufen, wenn die __enter__()-Methode erfolgreich abgeschlossen wird.
Statement-Templates führen ihre Ausnahmebehandlung in dieser Methode durch. Wenn das erste Argument None ist, deutet dies auf eine nicht-ausnahmebedingte Beendigung von BLOCK1 hin – die Ausführung hat entweder das Ende des Blocks erreicht, oder eine vorzeitige Beendigung wurde mit einer return, break oder continue-Anweisung erzwungen. Andernfalls spiegeln die drei Argumente die Ausnahme wider, die BLOCK1 beendet hat.
Alle von der __exit__()-Methode ausgelösten Ausnahmen werden an den Scope weitergegeben, der die with-Anweisung enthält. Wenn der Benutzercode in BLOCK1 ebenfalls eine Ausnahme ausgelöst hat, würde diese Ausnahme verloren gehen und durch die von der __exit__()-Methode ausgelöste ersetzt werden.
Faktorisierung beliebiger Ausnahmebehandlung
Betrachten Sie die folgende Anordnung der Ausnahmebehandlung
SETUP_BLOCK
try:
try:
TRY_BLOCK
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
Dies kann grob wie folgt in ein Statement-Template übersetzt werden
class my_template(object):
def __init__(self, *args):
# Any required arguments (e.g. a file name)
# get stored in member variables
# The various BLOCK's will need updating to reflect
# that.
def __enter__(self):
SETUP_BLOCK
def __exit__(self, exc_type, value, traceback):
try:
try:
if exc_type is not None:
raise exc_type, value, traceback
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
Was dann wie folgt verwendet werden kann
with my_template(*args):
TRY_BLOCK
Es gibt jedoch zwei wichtige semantische Unterschiede zwischen diesem Code und der ursprünglichen try-Anweisung.
Erstens, in der ursprünglichen try-Anweisung, wenn eine break-, return- oder continue-Anweisung in TRY_BLOCK angetroffen wird, wird nur FINALLY_BLOCK ausgeführt, während die Anweisung abgeschlossen wird. Mit dem Statement-Template wird auch ELSE_BLOCK ausgeführt, da diese Anweisungen wie jede andere nicht-ausnahmebedingte Blockbeendigung behandelt werden. Für Anwendungsfälle, bei denen dies wichtig ist, ist dies wahrscheinlich eine gute Sache (siehe transaction in den Beispielen), da diese Lücke, in der weder die except- noch die else-Klausel ausgeführt wird, leicht zu vergessen ist, wenn man Ausnahmebehandler schreibt.
Zweitens wird das Statement-Template keine Ausnahmen unterdrücken. Wenn beispielsweise der ursprüngliche Code die Ausnahmen exc_type1 und exc_type2 unterdrückt hätte, dann müsste dies immer noch inline im Benutzercode geschehen
try:
with my_template(*args):
TRY_BLOCK
except (exc_type1, exc_type2):
pass
Jedoch ist selbst in diesen Fällen, in denen die Unterdrückung von Ausnahmen explizit gemacht werden muss, die Menge an Boilerplate, die an der Aufrufstelle wiederholt wird, signifikant reduziert (Siehe Abgelehnte Optionen für weitere Diskussionen über dieses Verhalten).
Im Allgemeinen werden nicht alle Klauseln benötigt. Für die Ressourcenverwaltung (wie Dateien oder Synchronisationsschlösser) ist es möglich, den Code, der Teil von FINALLY_BLOCK in der __exit__()-Methode gewesen wäre, einfach auszuführen. Dies kann in der folgenden Implementierung gesehen werden, die Synchronisationsschlösser in Statement-Templates umwandelt, wie eingangs dieser Sektion erwähnt
# New methods of synchronisation lock objects
def __enter__(self):
self.acquire()
return self
def __exit__(self, *exc_info):
self.release()
Generatoren
Mit ihrer Fähigkeit, die Ausführung zu unterbrechen und die Kontrolle an den aufrufenden Frame zurückzugeben, sind Generatoren natürliche Kandidaten für die Erstellung von Statement-Templates. Die Hinzufügung benutzerdefinierter Anweisungen zur Sprache erfordert **nicht** die in diesem Abschnitt beschriebenen Generatoränderungen, was diesen PEP zu einem offensichtlichen Kandidaten für eine schrittweise Implementierung macht ( with-Anweisungen in Phase 1, Generatorintegration in Phase 2). Die vorgeschlagenen Generator-Updates ermöglichen die Faktorisierung beliebiger Ausnahmebehandlung auf diese Weise
@statement_template
def my_template(*arguments):
SETUP_BLOCK
try:
try:
yield
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
Beachten Sie, dass im Gegensatz zur klassenbasierten Version keiner der Blöcke modifiziert werden muss, da gemeinsame Werte lokale Variablen des internen Frames des Generators sind, einschließlich der von der aufrufenden Code übergebenen Argumente. Die zuvor genannten semantischen Unterschiede (alle nicht-ausnahmebedingten Blockbeendigungen lösen die else-Klausel aus und das Template kann keine Ausnahmen unterdrücken) gelten weiterhin.
Standardwert für yield
Beim Erstellen eines Statement-Templates mit einem Generator wird die yield-Anweisung oft nur dazu verwendet, die Kontrolle an den Körper der benutzerdefinierten Anweisung zurückzugeben, anstatt einen nützlichen Wert zurückzugeben.
Entsprechend, wenn dieser PEP akzeptiert wird, wird yield, wie return, einen Standardwert von None liefern (d. h. yield und yield None werden zu äquivalenten Anweisungen).
Diese gleiche Änderung wird in PEP 342 vorgeschlagen. Offensichtlich müsste sie nur einmal implementiert werden, wenn beide PEPs akzeptiert würden :)
Template-Generator-Decorator: statement_template
Wie bei PEP 343 wird ein neuer Decorator vorgeschlagen, der einen Generator in ein Objekt mit der entsprechenden Statement-Template-Semantik verpackt. Im Gegensatz zu PEP 343 sind die hier vorgeschlagenen Templates wiederverwendbar, da der Generator bei jedem Aufruf von __enter__() neu instanziiert wird. Zusätzlich werden alle Ausnahmen, die in BLOCK1 auftreten, im internen Frame des Generators erneut ausgelöst.
class template_generator_wrapper(object):
def __init__(self, func, func_args, func_kwds):
self.func = func
self.args = func_args
self.kwds = func_kwds
self.gen = None
def __enter__(self):
if self.gen is not None:
raise RuntimeError("Enter called without exit!")
self.gen = self.func(*self.args, **self.kwds)
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("Generator didn't yield")
def __exit__(self, *exc_info):
if self.gen is None:
raise RuntimeError("Exit called without enter!")
try:
try:
if exc_info[0] is not None:
self.gen._inject_exception(*exc_info)
else:
self.gen.next()
except StopIteration:
pass
else:
raise RuntimeError("Generator didn't stop")
finally:
self.gen = None
def statement_template(func):
def factory(*args, **kwds):
return template_generator_wrapper(func, args, kwds)
return factory
Template-Generator-Wrapper: __enter__()-Methode
Der Template-Generator-Wrapper hat eine __enter__()-Methode, die eine neue Instanz des enthaltenen Generators erstellt und dann next() einmal aufruft. Sie löst eine RuntimeError aus, wenn die letzte Generatorinstanz nicht aufgeräumt wurde oder wenn der Generator endet, anstatt einen Wert zu liefern.
Template-Generator-Wrapper: __exit__()-Methode
Die Template-Generator-Wrapper-Methode hat eine __exit__()-Methode, die einfach next() auf dem Generator aufruft, wenn keine Ausnahme übergeben wird. Wenn eine Ausnahme übergeben wird, wird sie im enthaltenen Generator an der Stelle der letzten yield-Anweisung erneut ausgelöst.
In beiden Fällen löst der Generator-Wrapper eine RuntimeError aus, wenn der interne Frame infolge der Operation nicht beendet wird. Die __exit__()-Methode räumt immer die Referenz auf die verwendete Generatorinstanz auf, was es ermöglicht, __enter__() erneut aufzurufen.
Eine von der __exit__()-Methode des Template-Generator-Wrappers ausgelöste StopIteration kann versehentlich unterdrückt werden, aber das ist unwichtig, da die ursprünglich ausgelöste Ausnahme weiterhin korrekt weitergegeben wird.
Ausnahmen in Generatoren injizieren
Um die __exit__()-Methode des Template-Generator-Wrappers zu implementieren, ist es notwendig, Ausnahmen in den internen Frame des Generators zu injizieren. Dies ist ein neues Verhalten auf Implementierungsebene, das derzeit keine Entsprechung in Python hat.
Der Injektionsmechanismus (in diesem PEP als _inject_exception bezeichnet) löst eine Ausnahme im Frame des Generators mit dem angegebenen Typ, Wert und Traceback-Informationen aus. Das bedeutet, dass die Ausnahme wie die ursprüngliche aussieht, wenn sie weitergegeben werden darf.
Für die Zwecke dieses PEP ist es nicht notwendig, diese Fähigkeit außerhalb des Python-Implementierungscodes verfügbar zu machen.
Generator-Finalisierung
Zur Unterstützung der Ressourcenverwaltung in Template-Generatoren wird in diesem PEP die Beschränkung für yield-Anweisungen innerhalb des try-Blocks einer try/finally-Anweisung aufgehoben. Folglich können Generatoren, die die Verwendung einer Datei oder eines ähnlichen Objekts erfordern, sicherstellen, dass das Objekt durch die Verwendung von try/finally- oder with-Anweisungen korrekt verwaltet wird.
Diese Einschränkung muss wahrscheinlich global aufgehoben werden – es wäre schwierig, sie so einzuschränken, dass sie nur innerhalb von Generatoren zulässig ist, die zur Definition von Statement-Templates verwendet werden. Entsprechend enthält dieser PEP Vorschläge, die sicherstellen, dass Generatoren, die nicht als Statement-Templates verwendet werden, dennoch ordnungsgemäß finalisiert werden.
Generator-Finalisierung: TerminateIteration-Ausnahme
Eine neue Ausnahme wird vorgeschlagen
class TerminateIteration(Exception): pass
Die neue Ausnahme wird in einen Generator injiziert, um die Finalisierung anzufordern. Sie sollte von gut funktionierendem Code nicht unterdrückt werden.
Generator-Finalisierung: __del__()-Methode
Um sicherzustellen, dass ein Generator irgendwann finalisiert wird (innerhalb der Grenzen der Python-Garbage-Collection), erhalten Generatoren eine __del__()-Methode mit folgender Semantik
def __del__(self):
try:
self._inject_exception(TerminateIteration, None, None)
except TerminateIteration:
pass
Deterministische Generator-Finalisierung
Es gibt eine einfache Möglichkeit, eine deterministische Finalisierung von Generatoren zu gewährleisten: ihnen entsprechende __enter__() und __exit__() Methoden zu geben
def __enter__(self):
return self
def __exit__(self, *exc_info):
try:
self._inject_exception(TerminateIteration, None, None)
except TerminateIteration:
pass
Dann kann jeder Generator prompt finalisiert werden, indem die relevante for-Schleife in eine with-Anweisung eingepackt wird
with all_lines(filenames) as lines:
for line in lines:
print lines
(Siehe die Beispiele für die Definition von all_lines und den Grund, warum sie eine sofortige Finalisierung erfordert)
Vergleichen Sie das obige Beispiel mit der Verwendung von Dateiobjekten
with open(filename) as f:
for line in f:
print f
Generatoren als benutzerdefinierte Statement-Templates
Wenn ein Generator zur Implementierung einer benutzerdefinierten Anweisung verwendet wird, sollte er auf jedem gegebenen Kontrollpfad nur einmal einen Wert liefern. Das Ergebnis dieser `yield`-Anweisung wird dann als Ergebnis der `__enter__()`-Methode des Generators bereitgestellt. Eine einzige `yield`-Anweisung auf jedem Kontrollpfad stellt sicher, dass der interne Frame beendet wird, wenn die `__exit__()`-Methode des Generators aufgerufen wird. Mehrere `yield`-Anweisungen auf einem einzigen Kontrollpfad führen dazu, dass die `__exit__()`-Methode eine `RuntimeError` auslöst, wenn der interne Frame nicht korrekt beendet wird. Ein solcher Fehler zeigt einen Fehler in der Statement-Vorlage an.
Um auf Ausnahmen zu reagieren oder Ressourcen freizugeben, reicht es aus, die yield-Anweisung in eine entsprechend konstruierte try-Anweisung zu packen. Wenn die Ausführung nach dem yield ohne Ausnahme fortgesetzt wird, weiß der Generator, dass der Körper der do-Anweisung ohne Zwischenfall abgeschlossen wurde.
Beispiele
- Eine Vorlage, um sicherzustellen, dass eine Sperre, die zu Beginn eines Blocks erworben wurde, freigegeben wird, wenn der Block verlassen wird
# New methods on synchronisation locks def __enter__(self): self.acquire() return self def __exit__(self, *exc_info): lock.release()
Verwendet wie folgt
with 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
# New methods on file objects def __enter__(self): if self.closed: raise RuntimeError, "Cannot reopen closed file handle" return self def __exit__(self, *args): self.close()
Verwendet wie folgt
with open("/etc/passwd") as f: for line in f: print line.rstrip()
- Eine Vorlage zum Committen oder Rollback einer Datenbanktransaktion
def transaction(db): try: yield except: db.rollback() else: db.commit()
Verwendet wie folgt
with transaction(the_db): make_table(the_db) add_data(the_db) # Getting to here automatically triggers a commit # Any exception automatically triggers a rollback
- Es ist möglich, Blöcke zu verschachteln und Templates zu kombinieren
@statement_template def lock_opening(lock, filename, mode="r"): with lock: with open(filename, mode) as f: yield f
Verwendet wie folgt
with lock_opening(myLock, "/etc/passwd") as f: for line in f: print line.rstrip()
- Stdout temporär umleiten
@statement_template def redirected_stdout(new_stdout): save_stdout = sys.stdout try: sys.stdout = new_stdout yield finally: sys.stdout = save_stdout
Verwendet wie folgt
with open(filename, "w") as f: with redirected_stdout(f): print "Hello world"
- Eine Variante von
open(), die auch einen Fehlerzustand zurückgibt@statement_template def open_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
do open_w_error("/etc/passwd", "a") as f, err: if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
- Finden Sie die erste Datei mit einem bestimmten Header
for name in filenames: with open(name) as f: if f.read(2) == 0xFEB0: break
- Finden Sie das erste Element, das Sie behandeln können, wobei ein Schloss für die gesamte Schleife oder nur für jede Iteration gehalten wird
with lock: for item in items: if handle(item): break for item in items: with lock: if handle(item): break
- Halten Sie ein Schloss, während Sie sich in einem Generator befinden, aber geben Sie es frei, wenn Sie die Kontrolle an den äußeren Scope zurückgeben
@statement_template def released(lock): lock.release() try: yield finally: lock.acquire()
Verwendet wie folgt
with lock: for item in items: with released(lock): yield item
- Zeilen aus einer Sammlung von Dateien lesen (z. B. Verarbeitung mehrerer Konfigurationsquellen)
def all_lines(filenames): for name in filenames: with open(name) as f: for line in f: yield line
Verwendet wie folgt
with all_lines(filenames) as lines: for line in lines: update_config(line)
- Nicht alle Verwendungen müssen Ressourcenverwaltung beinhalten
@statement_template def tag(*args, **kwds): name = cgi.escape(args[0]) if kwds: kwd_pairs = ["%s=%s" % cgi.escape(key), cgi.escape(value) for key, value in kwds] print '<%s %s>' % name, " ".join(kwd_pairs) else: print '<%s>' % name yield print '</%s>' % name
Verwendet wie folgt
with tag('html'): with tag('head'): with tag('title'): print 'A web page' with tag('body'): for par in pars: with tag('p'): print par with tag('a', href="https://pythonlang.de"): print "Not a dead parrot!"
- Aus PEP 343 wäre ein weiteres nützliches Beispiel eine Operation, die Signale blockiert. Die Verwendung könnte so aussehen:
from signal import blocked_signals with blocked_signals(): # code executed without worrying about signals
Ein optionales Argument könnte eine Liste von zu blockierenden Signalen sein; standardmäßig werden alle Signale blockiert. Die Implementierung wird als Übung für den Leser überlassen.
- Eine weitere Verwendungsmöglichkeit ist für Decimal-Kontexte
# New methods on decimal Context objects def __enter__(self): if self._old_context is not None: raise RuntimeError("Already suspending other Context") self._old_context = getcontext() setcontext(self) def __exit__(self, *args): setcontext(self._old_context) self._old_context = None
Verwendet wie folgt
with decimal.Context(precision=28): # Code here executes with the given context # The context always reverts after this statement
Offene Fragen
Keine, da dieser PEP zurückgezogen wurde.
Abgelehnte Optionen
Die grundlegende Konstruktion als Schleifenkonstrukt zu haben
Das Hauptproblem mit dieser Idee, wie durch PEP 340s block-Anweisungen veranschaulicht, ist, dass sie Probleme mit der Faktorisierung von try-Anweisungen innerhalb von Schleifen verursacht, die break- und continue-Anweisungen enthalten (da diese Anweisungen dann auf das block-Konstrukt und nicht auf die ursprüngliche Schleife angewendet würden). Da ein Hauptziel darin besteht, beliebige Ausnahmebehandlung (außer Unterdrückung) in Statement-Templates faktorisieren zu können, ist dies ein deutliches Problem.
Es gibt auch ein Verständlichkeitsproblem, wie in den Beispielen zu sehen ist. Im Beispiel, das den Erwerb eines Schlosses entweder für eine gesamte Schleife oder für jede Iteration der Schleife zeigt, würde die Verschiebung der benutzerdefinierten Anweisung von außerhalb der for-Schleife nach innen in die for-Schleife wesentliche semantische Auswirkungen haben, die über die erwarteten hinausgehen, wenn die benutzerdefinierte Anweisung selbst eine Schleife wäre.
Schließlich gibt es bei einem Schleifenkonstrukt erhebliche Probleme mit TOOWTDI (There's Only One Way To Do It), da oft unklar ist, ob eine bestimmte Situation mit einer herkömmlichen for-Schleife oder dem neuen Schleifenkonstrukt behandelt werden sollte. Mit dem aktuellen PEP gibt es kein solches Problem – for-Schleifen werden weiterhin für die Iteration verwendet, und die neuen do-Anweisungen werden verwendet, um Ausnahmebehandlung zu faktorisieren.
Ein weiteres Problem, insbesondere bei PEP 340s anonymen Blockanweisungen, ist, dass sie es sehr schwierig machen, Statement-Templates direkt zu schreiben (d. h. nicht mit einem Generator). Dieses Problem wird durch den aktuellen Vorschlag gelöst, wie die relative Einfachheit der verschiedenen klassenbasierten Implementierungen von Statement-Templates in den Beispielen zeigt.
Statement-Templates zuzulassen, Ausnahmen zu unterdrücken
Frühere Versionen dieses PEP gaben Statement-Templates die Möglichkeit, Ausnahmen zu unterdrücken. Der BDFL äußerte Bedenken hinsichtlich der damit verbundenen Komplexität, und ich stimmte zu, nachdem ich einen Artikel von Raymond Chen über die Übel des Verbergens von Flusskontrolle in Makros in C-Code gelesen hatte [1].
Das Entfernen der Unterdrückungsfähigkeit hat eine ganze Menge Komplexität sowohl aus der Erklärung als auch aus der Implementierung von benutzerdefinierten Statements entfernt, was es weiter als die richtige Wahl unterstützt. Ältere Versionen des PEP mussten einige schreckliche Hürden überwinden, um unbeabsichtigte Ausnahmen in __exit__() Methoden zu vermeiden - dieses Problem existiert mit den aktuellen vorgeschlagenen Semantiken nicht.
Es gab ein Beispiel (auto_retry), das tatsächlich die Fähigkeit zur Unterdrückung von Ausnahmen nutzte. Dieser Anwendungsfall ist zwar nicht ganz so elegant, hat aber einen deutlich offensichtlicheren Kontrollfluss, wenn er vollständig im Benutzercode ausgeschrieben wird.
def attempts(num_tries):
return reversed(xrange(num_tries))
for retry in attempts(3):
try:
make_attempt()
except IOError:
if not retry:
raise
Was es wert ist, die Perversen könnten dies immer noch so schreiben:
for attempt in auto_retry(3, IOError):
try:
with attempt:
make_attempt()
except FailedAttempt:
pass
Um die Unschuldigen zu schützen, ist der Code zur tatsächlichen Unterstützung hiervon hier nicht enthalten.
Unterscheidung zwischen nicht-ausnahmebedingten Beendigungen
Frühere Versionen dieses PEP erlaubten Statement-Vorlagen, zwischen dem normalen Verlassen des Blocks und dem Verlassen durch eine return, break oder continue Anweisung zu unterscheiden. Der BDFL spielte mit einer ähnlichen Idee in PEP 343 und der damit verbundenen Diskussion. Dies fügte der Beschreibung der Semantiken erhebliche Komplexität hinzu und erforderte, dass jede einzelne Statement-Vorlage entschied, ob diese Anweisungen wie Ausnahmen behandelt werden sollten oder wie ein normaler Mechanismus zum Verlassen des Blocks.
Dieser Template-zu-Template-Entscheidungsprozess führte zu großem Verwirrungspotenzial - stellen Sie sich vor, ein Datenbankverbinder würde eine Transaktionsvorlage bereitstellen, die frühe Ausgänge wie eine Ausnahme behandelt, während ein zweiter Verbinder sie als normale Blockterminierung behandelt.
Entsprechend verwendet dieser PEP nun die einfachste Lösung - frühe Ausgänge erscheinen aus Sicht der Statement-Vorlage identisch mit der normalen Blockterminierung.
Ausgelöste Ausnahmen nicht in Generatoren zu injizieren
PEP 343 schlägt vor, bei Generatoren, die zur Definition von Statement-Vorlagen verwendet werden, einfach bedingungslos next() aufzurufen. Das bedeutet, dass die Vorlagen-Generatoren eher unintuitiv aussehen, und die Beibehaltung des Verbots von yield innerhalb von try/finally bedeutet, dass die Ausnahmebehandlungsfähigkeiten von Python nicht zur Verwaltung mehrerer Ressourcen verwendet werden können.
Die Alternative, die dieser PEP befürwortet (Einspeisen von ausgelösten Ausnahmen in den Generator-Frame), bedeutet, dass mehrere Ressourcen elegant verwaltet werden können, wie durch lock_opening in den Beispielen gezeigt.
Alle Generatoren zu Statement-Templates zu machen
Die Trennung des Vorlageobjekts vom Generator selbst ermöglicht wiederverwendbare Vorlagen-Generatoren. Das heißt, der folgende Code funktioniert korrekt, wenn dieser PEP akzeptiert wird.
open_it = lock_opening(parrot_lock, "dead_parrot.txt")
with open_it as f:
# use the file for a while
with open_it as f:
# use the file again
Der zweite Vorteil ist, dass Iterator-Generatoren und Vorlagen-Generatoren sehr unterschiedliche Dinge sind - der Dekorator hält diese Unterscheidung klar und verhindert, dass einer dort verwendet wird, wo der andere benötigt wird.
Schließlich ermöglicht die Anforderung des Dekorators, dass die nativen Methoden von Generatorobjekten zur Implementierung der Generator-Finalisierung verwendet werden.
Das Schlüsselwort do zu verwenden
do war ein alternatives Schlüsselwort, das während der PEP 340 Diskussion vorgeschlagen wurde. Es liest sich gut mit entsprechend benannten Funktionen, aber es liest sich schlecht, wenn es mit Methoden oder mit Objekten verwendet wird, die native Statement-Vorlagenunterstützung bieten.
Als do zum ersten Mal vorgeschlagen wurde, hatte der BDFL PEP 310's with Schlüsselwort abgelehnt, basierend auf dem Wunsch, es für ein Pascal/Delphi-Stil with Statement zu verwenden. Seitdem hat der BDFL dieses Bedenken zurückgezogen, da er ein solches Statement nicht mehr beabsichtigt. Diese Herzensänderung basierte anscheinend auf den Gründen der C#-Entwickler, die Funktion nicht bereitzustellen [2].
Kein Schlüsselwort zu haben
Dies ist eine interessante Option und kann recht gut lesbar gemacht werden. Es ist jedoch für neue Benutzer umständlich in der Dokumentation nachzuschlagen und wirkt auf manche zu magisch. Entsprechend verfolgt dieser PEP einen schlüsselwortbasierten Vorschlag.
Verbesserung von try-Anweisungen
Dieser Vorschlag beinhaltet, try Anweisungen eine Signatur zu geben, die der für with Anweisungen vorgeschlagenen ähnelt.
Ich denke, dass der Versuch, ein with Statement als erweitertes try Statement zu schreiben, genauso sinnvoll ist, wie zu versuchen, eine for Schleife als erweitertes while Statement zu schreiben. Das heißt, während die Semantik des ersteren als eine bestimmte Art der Verwendung des letzteren erklärt werden kann, ist ersteres keine *Instanz* des letzteren. Die zusätzlichen Semantiken, die um das grundlegendere Statement hinzugefügt werden, ergeben ein neues Konstrukt, und die beiden unterschiedlichen Statements sollten nicht verwechselt werden.
Dies zeigt sich daran, dass das „erweiterte“ try Statement immer noch in Bezug auf ein „nicht-erweitertes“ try Statement erklärt werden muss. Wenn es etwas anderes ist, ist es sinnvoller, ihm einen anderen Namen zu geben.
Das Template-Protokoll direkt try-Anweisungen widerspiegeln zu lassen
Ein Vorschlag war, separate Methoden im Protokoll zu haben, die verschiedene Teile der Struktur eines verallgemeinerten try Statements abdecken. Unter Verwendung der Begriffe try, except, else und finally hätten wir etwas wie:
class my_template(object):
def __init__(self, *args):
# Any required arguments (e.g. a file name)
# get stored in member variables
# The various BLOCK's will need to updated to reflect
# that.
def __try__(self):
SETUP_BLOCK
def __except__(self, exc, value, traceback):
if isinstance(exc, exc_type1):
EXCEPT_BLOCK1
if isinstance(exc, exc_type2):
EXCEPT_BLOCK2
else:
EXCEPT_BLOCK3
def __else__(self):
ELSE_BLOCK
def __finally__(self):
FINALLY_BLOCK
Abgesehen davon, dass die Hinzufügung von zwei Methodenslots anstelle von vier bevorzugt wird, halte ich es für erheblich einfacher, einfach eine leicht modifizierte Version des ursprünglichen try Statement-Codes in der __exit__() Methode (wie in Factoring out arbitrary exception handling gezeigt) reproduzieren zu können, anstatt die Funktionalität auf mehrere verschiedene Methoden aufzuteilen (oder herauszufinden, welche Methode verwendet werden soll, wenn nicht alle Klauseln vom Template verwendet werden).
Um diese Diskussion weniger theoretisch zu gestalten, hier ist das transaction Beispiel, das sowohl mit dem Zwei-Methoden- als auch mit dem Vier-Methoden-Protokoll anstelle eines Generators implementiert ist. Beide Implementierungen garantieren einen Commit, wenn eine break, return oder continue Anweisung angetroffen wird (ebenso wie die Generator-basierte Implementierung im Abschnitt Beispiele).
class transaction_2method(object):
def __init__(self, db):
self.db = db
def __enter__(self):
pass
def __exit__(self, exc_type, *exc_details):
if exc_type is None:
self.db.commit()
else:
self.db.rollback()
class transaction_4method(object):
def __init__(self, db):
self.db = db
self.commit = False
def __try__(self):
self.commit = True
def __except__(self, exc_type, exc_value, traceback):
self.db.rollback()
self.commit = False
def __else__(self):
pass
def __finally__(self):
if self.commit:
self.db.commit()
self.commit = False
Es gibt noch zwei weitere kleinere Punkte, die sich auf die spezifischen Methodennamen im Vorschlag beziehen. Der Name der __try__() Methode ist irreführend, da SETUP_BLOCK *vor* dem Betreten der try Anweisung ausgeführt wird, und der Name der __else__() Methode ist isoliert betrachtet unklar, da zahlreiche andere Python-Anweisungen eine else Klausel enthalten.
Iterator-Finalisierung (ZURÜCKGEZOGEN)
Die Möglichkeit, benutzerdefinierte Statements innerhalb von Generatoren zu verwenden, wird wahrscheinlich die Notwendigkeit einer deterministischen Finalisierung von Iteratoren erhöhen, da die Ressourcenverwaltung in die Generatoren verlagert wird, anstatt wie bisher extern gehandhabt zu werden.
Der PEP schlägt derzeit vor, dies zu handhaben, indem alle Generatoren zu Statement-Vorlagen gemacht werden und with Statements zur Handhabung der Finalisierung verwendet werden. Frühere Versionen dieses PEP schlugen jedoch die folgende, komplexere Lösung vor, die es dem *Autor* eines Generators erlaubte, die Notwendigkeit der Finalisierung zu kennzeichnen, und for Schleifen dies automatisch behandeln ließ. Sie ist hier als lange, detaillierte abgelehnte Option enthalten.
Hinzufügung zum Iterator-Protokoll: __finish__
Eine optionale neue Methode für Iteratoren wird vorgeschlagen, namens __finish__(). Sie nimmt keine Argumente entgegen und sollte nichts zurückgeben.
Die Methode __finish__ soll alle vom Iterator geöffneten Ressourcen bereinigen. Iteratoren mit einer __finish__() Methode werden für den Rest des PEP als „finishable iterators“ bezeichnet.
Best-Effort-Finalisierung
Ein finishable Iterator sollte sicherstellen, dass er eine __del__ Methode bereitstellt, die ebenfalls eine Finalisierung durchführt (z.B. durch Aufruf der __finish__() Methode). Dies ermöglicht es Python, eine Best-Effort-Finalisierung durchzuführen, falls keine deterministische Finalisierung auf den Iterator angewendet wird.
Deterministische Finalisierung
Wenn der in einer for Schleife verwendete Iterator eine __finish__() Methode hat, garantieren die erweiterten for Schleifen-Semantiken, dass diese Methode ausgeführt wird, unabhängig von der Art des Verlassens der Schleife. Dies ist wichtig für Iterator-Generatoren, die benutzerdefinierte Statements oder die nun erlaubten try/finally Statements verwenden, oder für neue Iteratoren, die auf eine rechtzeitige Finalisierung angewiesen sind, um zugewiesene Ressourcen freizugeben (z.B. Freigabe eines Threads oder einer Datenbankverbindung zurück in einen Pool).
for-Schleifensyntax
Es werden keine Änderungen an der Syntax von for Schleifen vorgeschlagen. Dies dient lediglich dazu, die für die Beschreibung der Semantiken erforderlichen Statement-Teile zu definieren.
for VAR1 in EXPR1:
BLOCK1
else:
BLOCK2
Aktualisierte for-Schleifen-Semantik
Wenn der Zieliterator keine __finish__() Methode hat, wird eine for Schleife wie folgt ausgeführt (d.h. keine Änderung gegenüber dem Status quo).
itr = iter(EXPR1)
exhausted = False
while True:
try:
VAR1 = itr.next()
except StopIteration:
exhausted = True
break
BLOCK1
if exhausted:
BLOCK2
Wenn der Zieliterator eine __finish__() Methode hat, wird eine for Schleife wie folgt ausgeführt.
itr = iter(EXPR1)
exhausted = False
try:
while True:
try:
VAR1 = itr.next()
except StopIteration:
exhausted = True
break
BLOCK1
if exhausted:
BLOCK2
finally:
itr.__finish__()
Die Implementierung muss sorgfältig darauf achten, den try/finally Overhead zu vermeiden, wenn der Iterator keine __finish__() Methode hat.
Generator-Iterator-Finalisierung: __finish__()-Methode
Wenn Generatoren mit dem entsprechenden Dekorator aktiviert sind, erhalten sie eine __finish__() Methode, die TerminateIteration im internen Frame auslöst.
def __finish__(self):
try:
self._inject_exception(TerminateIteration)
except TerminateIteration:
pass
Ein Dekorator (z.B. needs_finish()) ist erforderlich, um diese Funktion zu aktivieren, damit bestehende Generatoren (die keine Finalisierung erwarten) wie erwartet funktionieren.
Partielle Iteration von finalisierbaren Iteratoren
Eine teilweise Iteration eines finishable Iterators ist möglich, erfordert jedoch etwas Sorgfalt, um sicherzustellen, dass der Iterator dennoch prompt finalisiert wird (er wurde aus einem Grund finishable!). Zuerst benötigen wir eine Klasse, um die teilweise Iteration eines finishable Iterators zu ermöglichen, indem wir die __finish__() Methode des Iterators vor der for Schleife verbergen.
class partial_iter(object):
def __init__(self, iterable):
self.iter = iter(iterable)
def __iter__(self):
return self
def next(self):
return self.itr.next()
Zweitens wird eine geeignete Statement-Vorlage benötigt, um sicherzustellen, dass der Iterator schließlich finalisiert wird.
@statement_template
def finishing(iterable):
itr = iter(iterable)
itr_finish = getattr(itr, "__finish__", None)
if itr_finish is None:
yield itr
else:
try:
yield partial_iter(itr)
finally:
itr_finish()
Dies kann dann wie folgt verwendet werden:
do finishing(finishable_itr) as itr:
for header_item in itr:
if end_of_header(header_item):
break
# process header item
for body_item in itr:
# process body item
Beachten Sie, dass keine der obigen Ausführungen für einen Iterator erforderlich ist, der nicht finishable ist - ohne eine __finish__() Methode wird er von der for Schleife nicht prompt finalisiert und erlaubt daher inhärent eine teilweise Iteration. Das Zulassen der teilweisen Iteration von nicht-finishable Iteratoren als Standardverhalten ist ein Schlüsselelement, um diese Ergänzung des Iterator-Protokolls abwärtskompatibel zu halten.
Danksagungen
Die Danksagungssektion für PEP 340 gilt, da dieser Text aus der Diskussion dieses PEPs hervorgegangen ist, aber zusätzliche Dank geht an Michael Hudson, Paul Moore und Guido van Rossum für das Verfassen von PEP 310 und PEP 340 überhaupt, und an (in keiner bestimmten Reihenfolge) Fredrik Lundh, Phillip J. Eby, Steven Bethard, Josiah Carlson, Greg Ewing, Tim Delaney und Arnold deVos für die Anregung bestimmter Ideen, die in diesen Text eingeflossen sind.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0346.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT