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

Python Enhancement Proposals

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

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.

Hinweis des Autors

Dieses PEP wurde ursprünglich in der Ich-Form von Guido geschrieben und anschließend von Alyssa (Nick) Coghlan aktualisiert, um spätere Diskussionen auf python-dev widerzuspiegeln. Alle Verweise in der Ich-Form stammen von Guidos Original.

Der Alpha-Release-Zyklus von Python deckte Terminologieprobleme in diesem PEP und in der zugehörigen Dokumentation und Implementierung auf [13]. Das PEP stabilisierte sich etwa zur Zeit des ersten Python 2.5 Beta-Releases.

Ja, die Verbform ist an einigen Stellen durcheinander. Wir arbeiten nun schon über ein Jahr an diesem PEP, daher sind Dinge, die ursprünglich in der Zukunft lagen, nun in der Vergangenheit :)

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.

  1. Welche Ausnahme sollte GeneratorContextManager auslösen, wenn der zugrunde liegende Generator-Iterator fehlerhaft ist? Das folgende Zitat ist der Grund für Guidos Wahl von RuntimeError sowohl für diesen als auch für die close()-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 RuntimeError auslö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).“

  2. Es ist in Ordnung, AttributeError anstelle von TypeError auszulö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-API TypeError anstelle von AttributeError auslöst, ist ein Zufall der Geschichte und keine bewusste Designentscheidung [11].
  3. Objekte mit __enter__/__exit__-Methoden werden als „Kontextmanager“ bezeichnet, und der Decorator zur Umwandlung einer Generatorfunktion in eine Kontextmanager-Fabrik ist contextlib.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.

  1. 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).
    
  2. 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()
    
  3. Eine Vorlage zum Committen oder Rollback einer Datenbanktransaktion
    @contextmanager
    def transaction(db):
        db.begin()
        try:
            yield None
        except:
            db.rollback()
            raise
        else:
            db.commit()
    
  4. 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.)

  5. 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.

  6. 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")
    
  7. 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.

  8. 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
    
  9. 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
    
  10. 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)

  11. 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 die acquire() und release() Aufrufe vertauscht werden
    class 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
    
  12. 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


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

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