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

Python Enhancement Proposals

PEP 377 – Allow __enter__() methods to skip the statement body

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
08-März 2009
Python-Version:
2.7, 3.1
Post-History:
08-März 2009

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt einen rückwärtskompatiblen Mechanismus vor, der es __enter__()-Methoden erlaubt, den Körper der zugehörigen with-Anweisung zu überspringen. Das Fehlen dieser Fähigkeit bedeutet derzeit, dass der Decorator contextlib.contextmanager seine Spezifikation nicht erfüllen kann, beliebigen Code in einen Context Manager zu verwandeln, indem er ihn in eine Generatorfunktion mit einem Yield an der entsprechenden Stelle verschiebt. Ein Symptom dafür ist, dass contextlib.nested derzeit RuntimeError in Situationen auslöst, in denen das Ausschreiben der entsprechenden verschachtelten with-Anweisungen dies nicht tun würde [1].

Die vorgeschlagene Änderung besteht darin, eine neue Flusskontroll-Ausnahme SkipStatement einzuführen und die Ausführung des Körpers der with-Anweisung zu überspringen, wenn __enter__() diese Ausnahme auslöst.

Ablehnung der PEP

Diese PEP wurde von Guido zurückgewiesen [4], da sie eine zu große Komplexitätserhöhung ohne eine proportionale Erhöhung der Ausdrucksstärke und Korrektheit mit sich bringt. In Abwesenheit zwingender Anwendungsfälle, die die komplexere Semantik dieser PEP erfordern, wird das bestehende Verhalten als akzeptabel angesehen.

Vorgeschlagene Änderung

Die Semantik der with-Anweisung wird geändert, um einen neuen try/except/else-Block um den Aufruf von __enter__() herum einzufügen. Wenn SkipStatement von der __enter__()-Methode ausgelöst wird, wird der Hauptteil der with-Anweisung (nun im else-Zweig) nicht ausgeführt. Um zu vermeiden, dass die Namen in einer as-Klausel in diesem Fall ungebunden bleiben, wird ein neues Singleton StatementSkipped (ähnlich dem bestehenden Singleton NotImplemented) allen Namen zugewiesen, die in der as-Klausel vorkommen.

Die Komponenten der with-Anweisung bleiben wie in PEP 343 beschrieben.

with EXPR as VAR:
    BLOCK

Nach der Modifikation würde die Semantik der with-Anweisung wie folgt aussehen:

mgr = (EXPR)
exit = mgr.__exit__  # Not calling it yet
try:
    value = mgr.__enter__()
except SkipStatement:
    VAR = StatementSkipped
    # Only if "as VAR" is present and
    # VAR is a single name
    # If VAR is a tuple of names, then StatementSkipped
    # will be assigned to each name in the tuple
else:
    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(*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(None, None, None)

Mit der oben genannten Änderung der Semantik der with-Anweisung würde contextlib.contextmanager() dann SkipStatement anstelle von RuntimeError auslösen, wenn der zugrunde liegende Generator nicht yieldet.

Begründung für die Änderung

Derzeit können einige scheinbar harmlose Context Manager RuntimeError auslösen, wenn sie ausgeführt werden. Dies geschieht, wenn die __enter__()-Methode des Context Managers auf eine Situation stößt, in der die ausgeschriebene Version des Codes, die dem Context Manager entspricht, den Code überspringen würde, der nun der Körper der with-Anweisung ist. Da die __enter__()-Methode keinen verfügbaren Mechanismus hat, um dies dem Interpreter zu signalisieren, ist sie gezwungen, stattdessen eine Ausnahme auszulösen, die nicht nur den Körper der with-Anweisung überspringt, sondern auch über allen Code bis zum nächsten Exception Handler springt. Dies widerspricht einem der Designziele der with-Anweisung, nämlich beliebigen gemeinsamen Exception-Handling-Code in einem einzigen Context Manager zu faktorisieren, indem er in eine Generatorfunktion gesteckt und der variable Teil des Codes durch eine yield-Anweisung ersetzt wird.

Insbesondere verhalten sich die folgenden Beispiele unterschiedlich, wenn cmB().__enter__() eine Ausnahme auslöst, die cmA().__exit__() dann behandelt und unterdrückt.

with cmA():
  with cmB():
    do_stuff()
# This will resume here without executing "do_stuff()"

@contextlib.contextmanager
def combined():
  with cmA():
    with cmB():
      yield

with combined():
  do_stuff()
# This will raise a RuntimeError complaining that the context
# manager's underlying generator didn't yield

with contextlib.nested(cmA(), cmB()):
  do_stuff()
# This will raise the same RuntimeError as the contextmanager()
# example (unsurprising, given that the nested() implementation
# uses contextmanager())

# The following class based version shows that the issue isn't
# specific to contextlib.contextmanager() (it also shows how
# much simpler it is to write context managers as generators
# instead of as classes!)
class CM(object):
  def __init__(self):
    self.cmA = None
    self.cmB = None

  def __enter__(self):
    if self.cmA is not None:
      raise RuntimeError("Can't re-use this CM")
    self.cmA = cmA()
    self.cmA.__enter__()
    try:
      self.cmB = cmB()
      self.cmB.__enter__()
    except:
      self.cmA.__exit__(*sys.exc_info())
      # Can't suppress in __enter__(), so must raise
      raise

  def __exit__(self, *args):
    suppress = False
    try:
      if self.cmB is not None:
        suppress = self.cmB.__exit__(*args)
    except:
      suppress = self.cmA.__exit__(*sys.exc_info()):
      if not suppress:
        # Exception has changed, so reraise explicitly
        raise
    else:
      if suppress:
        # cmB already suppressed the exception,
        # so don't pass it to cmA
        suppress = self.cmA.__exit__(None, None, None):
      else:
        suppress = self.cmA.__exit__(*args):
    return suppress

Mit der vorgeschlagenen semantischen Änderung würden die oben genannten Contextlib-basierten Beispiele dann „einfach funktionieren“, aber die klassenbasierte Version müsste geringfügig angepasst werden, um die neue Semantik zu nutzen.

class CM(object):
  def __init__(self):
    self.cmA = None
    self.cmB = None

  def __enter__(self):
    if self.cmA is not None:
      raise RuntimeError("Can't re-use this CM")
    self.cmA = cmA()
    self.cmA.__enter__()
    try:
      self.cmB = cmB()
      self.cmB.__enter__()
    except:
      if self.cmA.__exit__(*sys.exc_info()):
        # Suppress the exception, but don't run
        # the body of the with statement either
        raise SkipStatement
      raise

  def __exit__(self, *args):
    suppress = False
    try:
      if self.cmB is not None:
        suppress = self.cmB.__exit__(*args)
    except:
      suppress = self.cmA.__exit__(*sys.exc_info()):
      if not suppress:
        # Exception has changed, so reraise explicitly
        raise
    else:
      if suppress:
        # cmB already suppressed the exception,
        # so don't pass it to cmA
        suppress = self.cmA.__exit__(None, None, None):
      else:
        suppress = self.cmA.__exit__(*args):
    return suppress

Es gibt derzeit einen vorläufigen Vorschlag [3], eine Import-ähnliche Syntax zur with-Anweisung hinzuzufügen, um mehrere Context Manager in einer einzigen with-Anweisung zu ermöglichen, ohne contextlib.nested verwenden zu müssen. In diesem Fall hat der Compiler die Möglichkeit, einfach mehrere with-Anweisungen auf AST-Ebene zu emittieren, wodurch die Semantik von tatsächlich verschachtelten with-Anweisungen genau reproduziert werden kann. Eine solche Änderung würde das Problem, das die aktuelle PEP zu lösen versucht, jedoch eher hervorheben als lindern: Es wäre nicht möglich, contextlib.contextmanager zu verwenden, um solche with-Anweisungen zuverlässig auszufakten, da sie genau dieselben semantischen Unterschiede aufweisen würden wie beim combined()-Context Manager im obigen Beispiel.

Leistungsauswirkungen

Die Implementierung der neuen Semantik macht es notwendig, die Referenzen auf die __enter__ und __exit__ Methoden in temporären Variablen statt auf dem Stack zu speichern. Dies führt zu einer leichten Verschlechterung der Geschwindigkeit von with-Anweisungen im Vergleich zu Python 2.6/3.1. Die Implementierung eines benutzerdefinierten SETUP_WITH Opcodes würde jedoch Unterschiede zwischen den beiden Ansätzen aufheben (und die Geschwindigkeit dramatisch verbessern, indem mehr als ein Dutzend unnötige Durchläufe durch die Auswertungs-Schleife eliminiert werden).

Referenzimplementierung

Patch angehängt an Issue 5251 [1]. Dieser Patch verwendet nur bestehende Opcodes (d.h. keine SETUP_WITH).

Danksagungen

James William Pye hat sowohl das Problem aufgeworfen als auch den grundlegenden Umriss der in dieser PEP beschriebenen Lösung vorgeschlagen.

Referenzen


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

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