PEP 343 – Die „with“-Anweisung
- Autor:
- Guido van Rossum, Alyssa Coghlan
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 13. Mai 2005
- Python-Version:
- 2.5
- Post-History:
- 02. Jun 2005, 16. Okt 2005, 29. Okt 2005, 23. Apr 2006, 01. Mai 2006, 30. Jul 2006
Inhaltsverzeichnis
- Zusammenfassung
- Hinweis des Autors
- Einleitung
- Motivation und Zusammenfassung
- Anwendungsfälle
- Spezifikation: Die „with“-Anweisung
- Migrationsplan
- Generator-Decorator
- Kontextmanager in der Standardbibliothek
- Standardterminologie
- Caching von Kontextmanagern
- Gelöste Probleme
- Abgelehnte Optionen
- Beispiele
- Referenzimplementierung
- Danksagungen
- Referenzen
- Urheberrecht
Zusammenfassung
Dieses PEP fügt der Python-Sprache eine neue Anweisung „with“ hinzu, um die Standardverwendung von try/finally-Anweisungen auslagern zu können.
In diesem PEP bieten Kontextmanager __enter__() und __exit__()-Methoden, die beim Eintritt in und beim Austritt aus dem Körper der with-Anweisung aufgerufen werden.
Einleitung
Nach vielen Diskussionen über PEP 340 und Alternativen entschied ich mich, PEP 340 zurückzuziehen und schlug eine leichte Variante von PEP 310 vor. Nach weiteren Diskussionen habe ich einen Mechanismus zum Auslösen einer Ausnahme in einem ausgesetzten Generator mit einer throw()-Methode und einer close()-Methode, die eine neue GeneratorExit-Ausnahme auslöst, wieder hinzugefügt; diese Ergänzungen wurden erstmals auf python-dev in [2] vorgeschlagen und einstimmig genehmigt. Ich ändere auch das Schlüsselwort zu ‚with‘.
Nach Annahme dieses PEP wurden die folgenden PEPs wegen Überschneidungen abgelehnt
- PEP 310, Zuverlässige Erfassungs-/Freigabepaare. Dies ist der ursprüngliche Vorschlag für die with-Anweisung.
- PEP 319, Python Synchronize/Asynchronize Block. Seine Anwendungsfälle können durch geeignete with-Anweisungs-Controller vom aktuellen PEP abgedeckt werden: Für ‚synchronize‘ können wir die „locking“-Vorlage aus Beispiel 1 verwenden; für ‚asynchronize‘ können wir eine ähnliche „unlocking“-Vorlage verwenden. Ich glaube nicht, dass ein „anonymer“ Lock, der mit einem Codeblock verbunden ist, von großer Bedeutung ist; tatsächlich ist es vielleicht besser, immer explizit auf den verwendeten Mutex zu verweisen.
PEP 340 und PEP 346 überschnitten sich ebenfalls mit diesem PEP, wurden aber freiwillig zurückgezogen, als dieses PEP eingereicht wurde.
Einige Diskussionen über frühere Inkarnationen dieses PEP fanden im Python Wiki statt [3].
Motivation und Zusammenfassung
PEP 340, Anonyme Block-Anweisungen, kombinierte viele leistungsstarke Ideen: Generatoren als Blockvorlagen verwenden, Ausnahmsbehandlung und Finalisierung zu Generatoren hinzufügen und mehr. Neben Lob erhielt es viel Widerspruch von Leuten, die die Tatsache nicht mochten, dass es im Grunde eine (potenzielle) Schleifenkonstruktion war. Das bedeutete, dass break und continue in einem Block-Statement das Block-Statement brechen oder fortsetzen würden, selbst wenn es als nicht-schleifendes Ressourcenmanagement-Werkzeug verwendet wurde.
Aber der letzte Schlag kam, als ich Raymond Chens Tirade über Makros zur Ablaufsteuerung las [1]. Raymond argumentiert überzeugend, dass das Verstecken von Ablaufsteuerungen in Makros den Code unübersichtlich macht, und ich finde, dass sein Argument für Python genauso gilt wie für C. Mir wurde klar, dass PEP 340-Vorlagen alle Arten von Ablaufsteuerungen verstecken können; zum Beispiel fängt sein Beispiel 4 (auto_retry()) Ausnahmen ab und wiederholt den Block bis zu dreimal.
Die with-Anweisung von PEP 310 versteckt meiner Meinung nach jedoch **keine** Ablaufsteuerung: Während eine finally-Suite die Ablaufsteuerung vorübergehend unterbricht, wird die Ablaufsteuerung am Ende fortgesetzt, als ob die finally-Suite gar nicht da gewesen wäre.
Denken Sie daran, PEP 310 schlägt ungefähr diese Syntax vor (der Teil „VAR =“ ist optional)
with VAR = EXPR:
BLOCK
was ungefähr dazu übersetzt wird
VAR = EXPR
VAR.__enter__()
try:
BLOCK
finally:
VAR.__exit__()
Betrachten Sie nun dieses Beispiel
with f = open("/etc/passwd"):
BLOCK1
BLOCK2
Hier wissen wir, genau wie wenn die erste Zeile „if True“ statt dessen dort stünde, dass, wenn BLOCK1 ohne Ausnahme abgeschlossen wird, BLOCK2 erreicht wird; und wenn BLOCK1 eine Ausnahme auslöst oder ein nicht-lokales goto (ein break, continue oder return) ausführt, wird BLOCK2 **nicht** erreicht. Die Magie, die von der with-Anweisung am Ende hinzugefügt wird, ändert daran nichts.
(Sie fragen sich vielleicht, was passiert, wenn ein Fehler in der __exit__()-Methode eine Ausnahme auslöst? Dann ist alles verloren – aber das ist nicht schlimmer als bei anderen Ausnahmen; die Natur von Ausnahmen ist, dass sie **überall** auftreten können, und damit müssen Sie leben. Selbst wenn Sie fehlerfreien Code schreiben, kann eine KeyboardInterrupt-Ausnahme ihn zwischen zwei virtuellen Maschinen-Opcodes beenden.)
Dieses Argument hätte mich fast dazu bewogen, PEP 310 zu unterstützen, aber ich hatte noch eine Idee aus der Euphorie von PEP 340, die ich noch nicht loslassen wollte: Generatoren als „Vorlagen“ für Abstraktionen wie das Erwerben und Freigeben eines Locks oder das Öffnen und Schließen einer Datei zu verwenden, ist eine mächtige Idee, wie die Beispiele in diesem PEP zeigen.
Inspiriert von einem Gegenvorschlag zu PEP 340 von Phillip Eby versuchte ich, einen Decorator zu erstellen, der einen geeigneten Generator in ein Objekt mit den notwendigen __enter__() und __exit__()-Methoden verwandelt. Hier stieß ich auf ein Problem: Während es für das Locking-Beispiel nicht allzu schwierig war, war es für das Öffnungs-Beispiel unmöglich. Die Idee war, die Vorlage wie folgt zu definieren
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
und so zu verwenden
with f = opening(filename):
...read data from f...
Das Problem ist, dass in PEP 310 das Ergebnis des Aufrufs von EXPR direkt an VAR zugewiesen wird und dann die __exit__()-Methode von VAR beim Austritt aus BLOCK1 aufgerufen wird. Hier muss VAR eindeutig die geöffnete Datei empfangen, und das würde bedeuten, dass __exit__() eine Methode der Datei sein müsste.
Während dies mit einer Proxy-Klasse gelöst werden kann, ist dies umständlich und brachte mich zu der Erkenntnis, dass eine leicht andere Übersetzung das Schreiben des gewünschten Decorators zum Kinderspiel machen würde: Lassen Sie VAR das Ergebnis des Aufrufs der __enter__()-Methode empfangen und speichern Sie den Wert von EXPR, um später seine __exit__()-Methode aufzurufen. Dann kann der Decorator eine Instanz einer Wrapper-Klasse zurückgeben, deren __enter__()-Methode die next()-Methode des Generators aufruft und alles zurückgibt, was next() zurückgibt; die __exit__()-Methode der Wrapper-Instanz ruft next() erneut auf, erwartet aber, dass StopIteration ausgelöst wird. (Details unten im Abschnitt Optional Generator Decorator.)
Nun war das letzte Hindernis, dass die PEP 310-Syntax
with VAR = EXPR:
BLOCK1
irreführend wäre, da VAR **nicht** den Wert von EXPR empfängt. Aus PEP 340 entlehnt war es ein einfacher Schritt zu
with EXPR as VAR:
BLOCK1
Zusätzliche Diskussionen zeigten, dass die Leute es wirklich mochten, die Ausnahme im Generator „sehen“ zu können, selbst wenn es nur zum Protokollieren war; der Generator darf keinen weiteren Wert liefern, da die with-Anweisung nicht als Schleife verwendet werden darf (das Auslösen einer anderen Ausnahme ist geringfügig akzeptabel). Um dies zu ermöglichen, wird eine neue throw()-Methode für Generatoren vorgeschlagen, die ein bis drei Argumente entgegennimmt, die eine Ausnahme im üblichen Format (Typ, Wert, Traceback) darstellen und sie an der Stelle auslöst, an der der Generator ausgesetzt wird.
Wenn wir dies haben, ist es nur noch ein kleiner Schritt, eine weitere Generator-Methode, close(), vorzuschlagen, die throw() mit einer speziellen Ausnahme, GeneratorExit, aufruft. Dies weist den Generator an, sich zu beenden, und von dort aus ist es nur noch ein kleiner Schritt, vorzuschlagen, dass close() automatisch aufgerufen wird, wenn der Generator garbage collected wird.
Dann können wir schließlich eine yield-Anweisung innerhalb einer try-finally-Anweisung zulassen, da wir nun garantieren können, dass die finally-Klausel (eventuell) ausgeführt wird. Die üblichen Vorsichtsmaßnahmen bei der Finalisierung gelten – der Prozess kann abrupt beendet werden, ohne Objekte zu finalisieren, und Objekte können durch Zyklen oder Speicherlecks in der Anwendung für immer am Leben gehalten werden (im Gegensatz zu Zyklen oder Lecks in der Python-Implementierung, die vom GC übernommen werden).
Beachten Sie, dass wir nicht garantieren, dass die finally-Klausel sofort ausgeführt wird, nachdem das Generatorobjekt nicht mehr verwendet wird, obwohl dies in CPython der Fall sein wird. Dies ähnelt dem automatischen Schließen von Dateien: Während eine Referenzzählung implementierende Implementierung wie CPython ein Objekt sofort freigibt, sobald die letzte Referenz darauf verschwindet, machen Implementierungen, die andere GC-Algorithmen verwenden, keine solche Garantie. Dies gilt für Jython, IronPython und wahrscheinlich für Python, das auf Parrot läuft.
(Die Details der an Generatoren vorgenommenen Änderungen finden sich nun in PEP 342 und nicht in diesem PEP)
Anwendungsfälle
Siehe den Abschnitt Beispiele am Ende.
Spezifikation: Die „with“-Anweisung
Eine neue Anweisung wird mit der Syntax vorgeschlagen
with EXPR as VAR:
BLOCK
Hier sind ‚with‘ und ‚as‘ neue Schlüsselwörter; EXPR ist ein beliebiger Ausdruck (aber keine Ausdrucksliste) und VAR ist ein einzelnes Zuweisungsziel. Es kann **keine** kommaseparierte Sequenz von Variablen sein, aber es **kann** eine **in Klammern** gesetzte kommaseparierte Sequenz von Variablen sein. (Diese Einschränkung ermöglicht eine zukünftige Erweiterung der Syntax auf mehrere kommaseparierte Ressourcen, jeweils mit eigener optionaler as-Klausel.)
Der Teil „as VAR“ ist optional.
Die Übersetzung der obigen Anweisung ist
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
Hier sind die Kleinbuchstabenvariablen (mgr, exit, value, exc) interne Variablen und für den Benutzer nicht zugänglich; sie werden höchstwahrscheinlich als spezielle Register oder Stapelpositionen implementiert.
Die Details der obigen Übersetzung sollen die genaue Semantik vorschreiben. Wenn eine der relevanten Methoden nicht wie erwartet gefunden wird, löst der Interpreter eine AttributeError aus, in der Reihenfolge, in der sie versucht werden (__exit__, __enter__). Ebenso, wenn einer der Aufrufe eine Ausnahme auslöst, ist die Auswirkung genau wie im obigen Code. Schließlich, wenn BLOCK eine break, continue oder return-Anweisung enthält, wird die __exit__()-Methode mit drei None-Argumenten aufgerufen, genau als ob BLOCK normal abgeschlossen worden wäre. (D. h. diese „Pseudo-Ausnahmen“ werden von __exit__() nicht als Ausnahmen betrachtet.)
Wenn der Teil „as VAR“ der Syntax weggelassen wird, wird der Teil „VAR =“ der Übersetzung weggelassen (aber mgr.__enter__() wird trotzdem aufgerufen).
Die Aufrufkonvention für mgr.__exit__() lautet wie folgt. Wenn die finally-Suite durch normale Beendigung von BLOCK oder durch ein nicht-lokales goto (ein break, continue oder return in BLOCK) erreicht wurde, wird mgr.__exit__() mit drei None-Argumenten aufgerufen. Wenn die finally-Suite durch eine in BLOCK ausgelöste Ausnahme erreicht wurde, wird mgr.__exit__() mit drei Argumenten aufgerufen, die den Ausnahmetyp, den Wert und den Traceback darstellen.
WICHTIG: Wenn mgr.__exit__() einen „wahren“ Wert zurückgibt, wird die Ausnahme „geschluckt“. Das heißt, wenn sie „wahr“ zurückgibt, wird die Ausführung mit der nächsten Anweisung nach der with-Anweisung fortgesetzt, auch wenn innerhalb der with-Anweisung eine Ausnahme aufgetreten ist. Wenn die with-Anweisung jedoch über ein nicht-lokales goto (break, continue oder return) verlassen wurde, wird dieses nicht-lokale return wieder aufgenommen, wenn mgr.__exit__() unabhängig vom Rückgabewert zurückkehrt. Die Motivation für dieses Detail ist es, mgr.__exit__() zu ermöglichen, Ausnahmen zu schlucken, ohne es zu einfach zu machen (da der Standardrückgabewert None falsch ist und dies dazu führt, dass die Ausnahme erneut ausgelöst wird). Der Hauptanwendungsfall für das Schlucken von Ausnahmen ist es, den @contextmanager-Decorator so schreiben zu können, dass ein try/except-Block in einem dekorierten Generator genauso funktioniert, als ob der Körper des Generators inline an der Stelle der with-Anweisung erweitert worden wäre.
Die Motivation für die Übergabe der Ausnahmedetails an __exit__(), im Gegensatz zur argumentlosen __exit__() aus PEP 310, wurde durch den Anwendungsfall transactional(), Beispiel 3 unten, gegeben. Die Vorlage in diesem Beispiel muss die Transaktion je nachdem, ob eine Ausnahme aufgetreten ist oder nicht, committen oder zurückrollen. Anstatt nur eines booleschen Flags, das angibt, ob eine Ausnahme aufgetreten ist, übergeben wir die vollständigen Ausnahmeinformationen, beispielsweise zur Unterstützung einer Ausnahme-Logging-Einrichtung. Die Verwendung von sys.exc_info() zum Abrufen der Ausnahmeinformationen wurde abgelehnt; sys.exc_info() hat sehr komplexe Semantiken und es ist durchaus möglich, dass es die Ausnahmeinformationen für eine Ausnahme zurückgibt, die vor langer Zeit abgefangen wurde. Es wurde auch vorgeschlagen, ein zusätzliches boolesches Argument hinzuzufügen, um zwischen dem Erreichen des Endes von BLOCK und einem nicht-lokalen goto zu unterscheiden. Dies wurde als zu komplex und unnötig abgelehnt; ein nicht-lokales goto sollte für die Entscheidung zur Rücknahme einer Datenbanktransaktion als unzweifelhaft betrachtet werden.
Um die Verkettung von Kontexten in Python-Code zu erleichtern, der Kontextmanager direkt manipuliert, sollten __exit__()-Methoden den an sie übergebenen Fehler **nicht** erneut auslösen. Es liegt immer in der Verantwortung des **Aufrufers** der __exit__()-Methode, dies in diesem Fall zu tun.
Auf diese Weise kann der Aufrufer erkennen, ob die __exit__()-Aufrufung **fehlgeschlagen** ist (im Gegensatz zur erfolgreichen Bereinigung vor der Weitergabe des ursprünglichen Fehlers).
Wenn __exit__() ohne Fehler zurückkehrt, kann dies als Erfolg der __exit__()-Methode selbst interpretiert werden (unabhängig davon, ob der ursprüngliche Fehler weitergegeben oder unterdrückt wird).
Wenn __exit__() jedoch eine Ausnahme an seinen Aufrufer weitergibt, bedeutet dies, dass __exit__() **selbst** fehlgeschlagen ist. Daher sollten __exit__()-Methoden Fehler vermeiden, es sei denn, sie sind tatsächlich fehlgeschlagen. (Und das Zulassen des ursprünglichen Fehlers ist kein Fehlschlag.)
Migrationsplan
In Python 2.5 wird die neue Syntax nur erkannt, wenn eine future-Anweisung vorhanden ist
from __future__ import with_statement
Dies macht sowohl ‚with‘ als auch ‚as‘ zu Schlüsselwörtern. Ohne die future-Anweisung gibt die Verwendung von ‚with‘ oder ‚as‘ als Bezeichner eine Warnung nach stderr aus.
In Python 2.6 wird die neue Syntax immer erkannt; ‚with‘ und ‚as‘ sind immer Schlüsselwörter.
Generator-Decorator
Mit der Annahme von PEP 342 ist es möglich, einen Decorator zu schreiben, der es ermöglicht, einen Generator, der genau einmal liefert, zur Steuerung einer with-Anweisung zu verwenden. Hier ist ein Entwurf eines solchen Decorators
class GeneratorContextManager(object):
def __init__(self, gen):
self.gen = gen
def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")
def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But
# throw() has to raise the exception to signal
# propagation, so this fixes the impedance mismatch
# between the throw() protocol and the __exit__()
# protocol.
#
if sys.exc_info()[1] is not value:
raise
def contextmanager(func):
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper
Dieser Decorator könnte wie folgt verwendet werden
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here (however unlikely)
Eine robuste Implementierung dieses Decorators wird Teil der Standardbibliothek sein.
Kontextmanager in der Standardbibliothek
Es wäre möglich, bestimmten Objekten wie Dateien, Sockets und Locks __enter__() und __exit__()-Methoden zu verleihen, sodass anstatt zu schreiben
with locking(myLock):
BLOCK
man einfach schreiben könnte
with myLock:
BLOCK
Ich denke, wir sollten damit vorsichtig sein; es könnte zu Fehlern führen wie
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2
was nicht das tut, was man vielleicht denkt (f wird geschlossen, bevor BLOCK2 betreten wird).
Andererseits sind solche Fehler leicht zu diagnostizieren; zum Beispiel löst der obige Generator-Kontext-Decorator eine RuntimeError aus, wenn eine zweite with-Anweisung f.__enter__() erneut aufruft. Ein ähnlicher Fehler kann ausgelöst werden, wenn __enter__ auf ein geschlossenes Datei-Objekt aufgerufen wird.
Für Python 2.5 wurden die folgenden Typen als Kontextmanager identifiziert
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
Ein Kontextmanager wird auch dem Decimal-Modul hinzugefügt, um die Verwendung eines lokalen Decimal-Arithmetik-Kontextes innerhalb des Körpers einer with-Anweisung zu unterstützen und den ursprünglichen Kontext automatisch wiederherzustellen, wenn die with-Anweisung verlassen wird.
Standardterminologie
Dieses PEP schlägt vor, dass das Protokoll, das aus den __enter__() und __exit__()-Methoden besteht, als „Kontextmanagement-Protokoll“ bezeichnet wird und Objekte, die dieses Protokoll implementieren, als „Kontextmanager“ bezeichnet werden. [4]
Der Ausdruck, der unmittelbar auf das Schlüsselwort with in der Anweisung folgt, ist ein „Kontextausdruck“, da dieser Ausdruck den Hauptanhaltspunkt für die Laufzeitumgebung liefert, die der Kontextmanager für die Dauer des Anweisungskörpers einrichtet.
Der Code im Körper der with-Anweisung und der Variablenname (oder die Namen) nach dem Schlüsselwort as haben zu diesem Zeitpunkt keine besonderen Begriffe. Die allgemeinen Begriffe „Anweisungskörper“ und „Zieliste“ können verwendet werden, prefixiert mit „with“ oder „with-Anweisung“, wenn die Begriffe sonst unklar wären.
Angesichts der Existenz von Objekten wie dem arithmetischen Kontext des Decimal-Moduls ist der Begriff „Kontext“ leider mehrdeutig. Bei Bedarf kann er spezifischer gemacht werden, indem die Begriffe „Kontextmanager“ für das konkrete Objekt, das durch den Kontextausdruck erstellt wird, und „Laufzeitkontext“ oder (vorzugsweise) „Laufzeitumgebung“ für die tatsächlichen Zustandsänderungen, die vom Kontextmanager vorgenommen werden, verwendet werden. Wenn einfach die Verwendung der with-Anweisung diskutiert wird, sollte die Mehrdeutigkeit keine große Rolle spielen, da der Kontextausdruck die Änderungen an der Laufzeitumgebung vollständig definiert. Die Unterscheidung ist wichtiger, wenn die Mechanik der with-Anweisung selbst und die Art und Weise, wie Kontextmanager tatsächlich implementiert werden, diskutiert werden.
Caching von Kontextmanagern
Viele Kontextmanager (wie Dateien und generatorbasierte Kontexte) werden Einwegobjekte sein. Sobald die __exit__()-Methode aufgerufen wurde, ist der Kontextmanager nicht mehr in einem benutzbaren Zustand (z. B. die Datei wurde geschlossen oder der zugrunde liegende Generator hat die Ausführung beendet).
Das Erfordernis eines neuen Manager-Objekts für jede with-Anweisung ist der einfachste Weg, Probleme mit multithreaded Code und verschachtelten with-Anweisungen zu vermeiden, die versuchen, denselben Kontextmanager zu verwenden. Es ist kein Zufall, dass alle Kontextmanager der Standardbibliothek, die Wiederverwendung unterstützen, aus dem Threading-Modul stammen – sie sind alle bereits darauf ausgelegt, mit den Problemen umzugehen, die durch Threading und verschachtelte Nutzung entstehen.
Das bedeutet, dass es typischerweise notwendig ist, einen Kontextmanager mit bestimmten Initialisierungsargumenten für die Verwendung in mehreren with-Anweisungen zu speichern, indem er in einem aufrufbaren Objekt ohne Argumente gespeichert wird, das dann im Kontextausdruck jeder Anweisung aufgerufen wird, anstatt den Kontextmanager direkt zu cachen.
Wenn diese Einschränkung nicht zutrifft, sollte die Dokumentation des betroffenen Kontextmanagers dies klarstellen.
Gelöste Probleme
Die folgenden Probleme wurden durch die Zustimmung des BDFL (und das Fehlen nennenswerter Einwände auf python-dev) gelöst.
- Welche Ausnahme sollte
GeneratorContextManagerauslösen, wenn der zugrunde liegende Generator-Iterator fehlerhaft ist? Das folgende Zitat ist der Grund für Guidos Wahl vonRuntimeErrorsowohl für diesen als auch für dieclose()-Methode des Generators in PEP 342 (aus [8])„Ich möchte keine neue Ausnahmeklasse für diesen Zweck einführen, da dies keine Ausnahme ist, die ich von Leuten erwarten möchte, die sie abfangen: Ich möchte, dass sie zu einem Traceback wird, der vom Programmierer gesehen wird, der den Code dann korrigiert. Daher glaube ich jetzt, dass beide
RuntimeErrorauslösen sollten. Es gibt einige Präzedenzfälle dafür: Sie wird vom Kern-Python-Code in Situationen ausgelöst, in denen endlose Rekursion erkannt wird, und für uninitialisierte Objekte (und für eine Vielzahl von sonstigen Bedingungen).“ - Es ist in Ordnung,
AttributeErroranstelle vonTypeErrorauszulösen, wenn die relevanten Methoden auf einer Klasse, die an einer with-Anweisung beteiligt ist, nicht vorhanden sind. Die Tatsache, dass die abstrakte Objekt-C-APITypeErroranstelle vonAttributeErrorauslöst, ist ein Zufall der Geschichte und keine bewusste Designentscheidung [11]. - Objekte mit
__enter__/__exit__-Methoden werden als „Kontextmanager“ bezeichnet, und der Decorator zur Umwandlung einer Generatorfunktion in eine Kontextmanager-Fabrik istcontextlib.contextmanager. Es gab einige andere Vorschläge [15] während des Release-Zyklus von 2.5, aber es wurden keine zwingenden Argumente vorgebracht, um von den im PEP-Implementierung verwendeten Begriffen abzuweichen.
Abgelehnte Optionen
Mehrere Monate lang verbot das PEP die Unterdrückung von Ausnahmen, um versteckte Ablaufsteuerungen zu vermeiden. Die Implementierung erwies sich als große Qual, also stellte Guido die Fähigkeit wieder her [12].
Ein weiterer Aspekt des PEP, der endlose Fragen und Terminologiedebatten hervorrief, war die Bereitstellung einer __context__()-Methode, die analog zur __iter__()-Methode eines Iterierbaren war [5] [7] [9]. Die anhaltenden Probleme [10] [12] mit der Erklärung, was es war, warum es existierte und wie es funktionieren sollte, führten schließlich dazu, dass Guido das Konzept endgültig aufgab [14] (und es gab große Freude!).
Die Idee, die PEP 342-Generator-API direkt zur Definition der with-Anweisung zu verwenden, wurde ebenfalls kurz erwogen [6], aber schnell verworfen, da dies das Schreiben von nicht-generatorbasierten Kontextmanagern zu schwierig machte.
Beispiele
Die generatorbasierten Beispiele stützen sich auf PEP 342. Außerdem sind einige der Beispiele in der Praxis unnötig, da die entsprechenden Objekte, wie z. B. threading.RLock, direkt in with-Anweisungen verwendet werden können.
Die im Namen der Beispielkontexte verwendete Zeitform ist nicht willkürlich. Die Vergangenheitsform („-ed“) wird verwendet, wenn der Name sich auf eine Aktion bezieht, die in der __enter__-Methode ausgeführt und in der __exit__-Methode rückgängig gemacht wird. Die Verlaufsform („-ing“) wird verwendet, wenn der Name sich auf eine Aktion bezieht, die in der __exit__-Methode ausgeführt werden soll.
- Eine Vorlage, um sicherzustellen, dass eine Sperre, die zu Beginn eines Blocks erworben wurde, freigegeben wird, wenn der Block verlassen wird
@contextmanager def locked(lock): lock.acquire() try: yield finally: lock.release()
Verwendet wie folgt
with locked(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
@contextmanager def opened(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
Verwendet wie folgt
with opened("/etc/passwd") as f: for line in f: print line.rstrip()
- Eine Vorlage zum Committen oder Rollback einer Datenbanktransaktion
@contextmanager def transaction(db): db.begin() try: yield None except: db.rollback() raise else: db.commit()
- Beispiel 1, umgeschrieben ohne Generator
class locked: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire() def __exit__(self, type, value, tb): self.lock.release()
(Dieses Beispiel kann leicht modifiziert werden, um die anderen relativ zustandslosen Beispiele zu implementieren; es zeigt, dass es einfach ist, die Notwendigkeit eines Generators zu vermeiden, wenn kein spezieller Zustand erhalten werden muss.)
- Stdout temporär umleiten
@contextmanager def stdout_redirected(new_stdout): save_stdout = sys.stdout sys.stdout = new_stdout try: yield None finally: sys.stdout = save_stdout
Verwendet wie folgt
with opened(filename, "w") as f: with stdout_redirected(f): print "Hello world"
Dies ist natürlich nicht threadsicher, aber auch das manuelle Ausführen des gleichen Tanzes ist es nicht. In Single-Threaded-Programmen (z. B. in Skripten) ist dies eine beliebte Vorgehensweise.
- Eine Variante von
opened(), die auch einen Fehlerzustand zurückgibt@contextmanager def opened_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
with opened_w_error("/etc/passwd", "a") as (f, err): if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
- Ein weiteres nützliches Beispiel wäre eine Operation, die Signale blockiert. Die Verwendung könnte so aussehen
import signal with signal.blocked(): # code executed without worrying about signals
Ein optionales Argument könnte eine Liste von Signalen sein, die blockiert werden sollen; standardmäßig werden alle Signale blockiert. Die Implementierung wird dem Leser als Übung überlassen.
- Ein weiterer Anwendungsfall für diese Funktion ist der Dezimalkontext. Hier ist ein einfaches Beispiel, nach einem von Michael Chermside geposteten Beispiel
import decimal @contextmanager def extra_precision(places=2): c = decimal.getcontext() saved_prec = c.prec c.prec += places try: yield None finally: c.prec = saved_prec
Beispielhafte Nutzung (angepasst aus der Python Library Reference)
def sin(x): "Return the sine of x as measured in radians." with extra_precision(): i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1 while s != lasts: lasts = s i += 2 fact *= i * (i-1) num *= x * x sign *= -1 s += num / fact * sign # The "+s" rounds back to the original precision, # so this must be outside the with-statement: return +s
- Hier ist ein einfacher Kontextmanager für das Dezimalmodul
@contextmanager def localcontext(ctx=None): """Set a new local decimal context for the block""" # Default to using the current context if ctx is None: ctx = getcontext() # We set the thread context to a copy of this context # to ensure that changes within the block are kept # local to the block. newctx = ctx.copy() oldctx = decimal.getcontext() decimal.setcontext(newctx) try: yield newctx finally: # Always restore the original context decimal.setcontext(oldctx)
Beispielhafte Nutzung
from decimal import localcontext, ExtendedContext def sin(x): with localcontext() as ctx: ctx.prec += 2 # Rest of sin calculation algorithm # uses a precision 2 greater than normal return +s # Convert result to normal precision def sin(x): with localcontext(ExtendedContext): # Rest of sin calculation algorithm # uses the Extended Context from the # General Decimal Arithmetic Specification return +s # Convert result to normal context
- Ein generischer „Objekt-Schließungs“-Kontextmanager
class closing(object): def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj def __exit__(self, *exc_info): try: close_it = self.obj.close except AttributeError: pass else: close_it()
Dies kann verwendet werden, um alles mit einer close-Methode deterministisch zu schließen, sei es eine Datei, ein Generator oder etwas anderes. Es kann sogar verwendet werden, wenn das Objekt nicht garantiert geschlossen werden muss (z. B. eine Funktion, die ein beliebiges iterierbares Objekt akzeptiert)
# emulate opening(): with closing(open("argument.txt")) as contradiction: for line in contradiction: print line # deterministically finalize an iterator: with closing(iter(data_source)) as data: for datum in data: process(datum)
(Das Kontextlib-Modul von Python 2.5 enthält eine Version dieses Kontextmanagers)
- PEP 319 gibt einen Anwendungsfall für die zusätzliche
released()Kontext, um eine zuvor erworbene Sperre vorübergehend freizugeben; dies kann sehr ähnlich zum obigen gesperrten Kontextmanager geschrieben werden, indem dieacquire()undrelease()Aufrufe vertauscht werdenclass released: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.release() def __exit__(self, type, value, tb): self.lock.acquire()
Beispielhafte Nutzung
with my_lock: # Operations with the lock held with released(my_lock): # Operations without the lock # e.g. blocking I/O # Lock is held again here
- Ein „verschachtelter“ Kontextmanager, der die bereitgestellten Kontexte von links nach rechts automatisch verschachtelt, um eine übermäßige Einrückung zu vermeiden
@contextmanager def nested(*contexts): exits = [] vars = [] try: try: for context in contexts: exit = context.__exit__ enter = context.__enter__ vars.append(enter()) exits.append(exit) yield vars except: exc = sys.exc_info() else: exc = (None, None, None) finally: while exits: exit = exits.pop() try: exit(*exc) except: exc = sys.exc_info() else: exc = (None, None, None) if exc != (None, None, None): # sys.exc_info() may have been # changed by one of the exit methods # so provide explicit exception info raise exc[0], exc[1], exc[2]
Beispielhafte Nutzung
with nested(a, b, c) as (x, y, z): # Perform operation
Entspricht
with a as x: with b as y: with c as z: # Perform operation
(Das Kontextlib-Modul von Python 2.5 enthält eine Version dieses Kontextmanagers)
Referenzimplementierung
Dieser PEP wurde zuerst von Guido bei seiner EuroPython-Keynote am 27. Juni 2005 angenommen. Er wurde später erneut angenommen, mit der hinzugefügten __context__ Methode. Die PEP wurde in Subversion für Python 2.5a1 implementiert. Die __context__() Methode wurde in Python 2.5b1 entfernt.
Danksagungen
Viele Leute trugen zu den Ideen und Konzepten dieser PEP bei, einschließlich all jener, die in den Danksagungen für PEP 340 und PEP 346 erwähnt werden.
Zusätzlicher Dank geht an (in keiner bestimmten Reihenfolge): Paul Moore, Phillip J. Eby, Greg Ewing, Jason Orendorff, Michael Hudson, Raymond Hettinger, Walter Dörwald, Aahz, Georg Brandl, Terry Reedy, A.M. Kuchling, Brett Cannon und all jene, die an den Diskussionen auf python-dev teilnahmen.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0343.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT