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

Python Enhancement Proposals

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

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.

Hinweis des Autors

Während der Diskussion von PEP 340 habe ich Entwürfe dieses PEP als PEP 3XX auf meiner eigenen Website gepflegt (da ich keinen CVS-Zugriff hatte, um einen eingereichten PEP schnell genug zu aktualisieren, um die Aktivität auf python-dev zu verfolgen).

Seit dem ersten Entwurf dieses PEP hat Guido PEP 343 als eine vereinfachte Version von PEP 340 geschrieben. PEP 343 (zum Zeitpunkt des Schreibens) verwendet für die neuen Anweisungen exakt dieselbe Semantik wie dieser PEP, verwendet jedoch einen leicht unterschiedlichen Mechanismus, um Generatoren zur Erstellung von Statement-Templates zu verwenden. Guido hat jedoch angegeben, dass er beabsichtigt, einen neuen PEP von Raymond Hettinger zu akzeptieren, der PEP 288 und PEP 325 integrieren und einen Generator-Decorator wie den in diesem PEP beschriebenen zulassen wird, um Statement-Templates für PEP 343 zu schreiben. Der andere Unterschied war die Wahl des Schlüsselworts ('with' im Gegensatz zu 'do') und Guido hat erklärt, dass er eine Abstimmung darüber im Kontext von PEP 343 organisieren wird.

Entsprechend wird die Version dieses PEP, die zur Archivierung auf python.org eingereicht wird, unmittelbar nach der Einreichung ZURÜCKGEZOGEN. PEP 343 und der kombinierte PEP zur Generatorerweiterung werden die wichtigen Ideen abdecken.

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

  1. 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).
    
  2. 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()
    
  3. 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
    
  4. 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()
    
  5. 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"
    
  6. 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")
    
  7. Finden Sie die erste Datei mit einem bestimmten Header
    for name in filenames:
        with open(name) as f:
            if f.read(2) == 0xFEB0:
                break
    
  8. 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
    
  9. 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
    
  10. 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)
    
  11. 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!"
    
  12. 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.

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


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

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