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

Python Enhancement Proposals

PEP 334 – Einfache Koroutinen mittels SuspendIteration

Autor:
Clark C. Evans <cce at clarkevans.com>
Status:
Zurückgezogen
Typ:
Standards Track
Erstellt:
26-Aug-2004
Python-Version:
3.0
Post-History:


Inhaltsverzeichnis

Zusammenfassung

Asynchrone Anwendungs-Frameworks wie Twisted [1] und Peak [2] basieren auf kooperativem Multitasking über Ereigniswarteschlangen oder verzögerte Ausführung. Während dieser Ansatz zur Anwendungsentwicklung keine Threads beinhaltet und somit eine ganze Klasse von Problemen vermeidet [3], schafft er eine andere Art von Programmierherausforderung. Wenn eine I/O-Operation blockieren würde, muss eine Benutzeranfrage unterbrochen werden, damit andere Anfragen fortfahren können. Das Konzept einer Koroutine [4] verspricht, dem Anwendungsentwickler bei dieser Schwierigkeit der Zustandsverwaltung zu helfen.

Dieses PEP schlägt einen begrenzten Ansatz für Koroutinen vor, der auf einer Erweiterung des Iterator-Protokolls basiert. Derzeit kann ein Iterator eine StopIteration-Ausnahme auslösen, um anzuzeigen, dass er keine Werte mehr liefert. Dieser Vorschlag fügt diesem Protokoll eine weitere Ausnahme hinzu, SuspendIteration, die anzeigt, dass der gegebene Iterator möglicherweise noch weitere Werte zu liefern hat, aber derzeit nicht dazu in der Lage ist.

Begründung

Es gibt derzeit zwei Ansätze, um Koroutinen nach Python zu bringen. Christian Tismers Stackless [6] beinhaltet eine grundlegende Umstrukturierung des Python-Ausführungsmodells durch Hacking des 'C'-Stacks. Während dieser Ansatz funktioniert, ist seine Funktionsweise schwer zu beschreiben und portabel zu halten. Ein ähnlicher Ansatz besteht darin, Python-Code für Parrot [7] zu kompilieren, einer Register-basierten virtuellen Maschine, die Koroutinen besitzt. Leider sind keine dieser Lösungen mit IronPython (CLR) oder Jython (JavaVM) portabel.

Es wird davon ausgegangen, dass ein begrenzterer Ansatz, der auf Iteratoren basiert, Anwendungsentwicklern eine Koroutinen-Einrichtung bieten könnte und dennoch über Laufzeiten hinweg portabel wäre.

  • Iteratoren speichern ihren Zustand in lokalen Variablen, die sich nicht auf dem "C"-Stack befinden. Iteratoren können als Klassen betrachtet werden, deren Zustand in Mitgliedsvariablen gespeichert ist, die über Aufrufe seiner next()-Methode hinaus bestehen bleiben.
  • Während eine nicht abgefangene Ausnahme die Ausführung einer Funktion beenden kann, muss eine nicht abgefangene Ausnahme einen Iterator nicht ungültig machen. Die vorgeschlagene Ausnahme, SuspendIteration, nutzt diese Funktion. Mit anderen Worten, nur weil ein Aufruf von next() zu einer Ausnahme führt, bedeutet das nicht unbedingt, dass der Iterator selbst nicht mehr in der Lage ist, Werte zu liefern.

Es gibt vier Stellen, an denen diese neue Ausnahme Auswirkungen hat

  • Der einfache Generator-Mechanismus von PEP 255 könnte erweitert werden, um diese SuspendIteration-Ausnahme sicher abzufangen, seinen aktuellen Zustand zu speichern und die Ausnahme an den Aufrufer weiterzugeben.
  • Verschiedene Iterator-Filter [9] in der Standardbibliothek, wie z.B. itertools.izip, sollten über diese Ausnahme informiert werden, damit sie SuspendIteration transparent weiterleiten kann.
  • Aus I/O-Operationen generierte Iteratoren, wie z.B. ein Datei- oder Socket-Leser, könnten modifiziert werden, um eine nicht blockierende Variante zu haben. Diese Option würde eine Unterklasse von SuspendIteration auslösen, wenn die angeforderte Operation blockieren würde.
  • Die asyncore-Bibliothek könnte aktualisiert werden, um einen grundlegenden "Runner" bereitzustellen, der aus einem Iterator zieht; wenn die SuspendIteration-Ausnahme abgefangen wird, dann geht er zum nächsten Iterator in seiner Runliste über [10]. Externe Frameworks wie Twisted würden alternative Implementierungen bereitstellen, möglicherweise basierend auf FreeBSD's kqueue oder Linux's epoll.

Auch wenn dies dramatisch erscheinen mag, ist es im Vergleich zum Nutzen von Continuations ein sehr geringer Arbeitsaufwand.

Semantik

Dieser Abschnitt erklärt auf hoher Ebene, wie die Einführung dieser neuen SuspendIteration-Ausnahme funktionieren würde.

Einfache Iteratoren

Die aktuelle Funktionalität von Iteratoren lässt sich am besten mit einem einfachen Beispiel verdeutlichen, das die beiden Werte 'one' und 'two' liefert.

class States:

    def __iter__(self):
        self._next = self.state_one
        return self

    def next(self):
        return self._next()

    def state_one(self):
        self._next = self.state_two
        return "one"

    def state_two(self):
        self._next = self.state_stop
        return "two"

    def state_stop(self):
        raise StopIteration

print list(States())

Eine äquivalente Iteration könnte natürlich durch den folgenden Generator erzeugt werden

def States():
    yield 'one'
    yield 'two'

print list(States())

Einführung von SuspendIteration

Angenommen, zwischen der Lieferung von 'one' und 'two' könnte der obige Generator bei einem Socket-Read blockieren. In diesem Fall würden wir SuspendIteration auslösen wollen, um zu signalisieren, dass der Iterator noch nicht fertig ist, aber derzeit keinen Wert liefern kann.

from random import randint
from time import sleep

class SuspendIteration(Exception):
      pass

class NonBlockingResource:

    """Randomly unable to produce the second value"""

    def __iter__(self):
        self._next = self.state_one
        return self

    def next(self):
        return self._next()

    def state_one(self):
        self._next = self.state_suspend
        return "one"

    def state_suspend(self):
        rand = randint(1,10)
        if 2 == rand:
            self._next = self.state_two
            return self.state_two()
        raise SuspendIteration()

    def state_two(self):
        self._next = self.state_stop
        return "two"

    def state_stop(self):
        raise StopIteration

def sleeplist(iterator, timeout = .1):
    """
    Do other things (e.g. sleep) while resource is
    unable to provide the next value
    """
    it = iter(iterator)
    retval = []
    while True:
        try:
            retval.append(it.next())
        except SuspendIteration:
            sleep(timeout)
            continue
        except StopIteration:
            break
    return retval

print sleeplist(NonBlockingResource())

In einer realen Situation wäre die NonBlockingResource ein Datei-Iterator, ein Socket-Handle oder ein anderer E/A-basierter Produzent. Die Sleeplist wäre stattdessen ein Asynchron-Reaktor, wie er in asyncore oder Twisted zu finden ist. Die nicht-blockierende Ressource könnte natürlich als Generator geschrieben werden

def NonBlockingResource():
    yield "one"
    while True:
        rand = randint(1,10)
        if 2 == rand:
            break
        raise SuspendIteration()
    yield "two"

Es ist nicht notwendig, ein Schlüsselwort wie 'suspend' hinzuzufügen, da die meisten realen Content-Generatoren nicht im Anwendungscode liegen, sondern in E/A-basierten Low-Level-Operationen. Da die meisten Programmierer nicht mit dem SuspendIteration()-Mechanismus konfrontiert werden müssen, ist ein Schlüsselwort nicht erforderlich.

Anwendungs-Iteratoren

Das vorherige Beispiel ist eher künstlich, ein "realeres" Beispiel wäre ein Webseiten-Generator, der HTML-Inhalt liefert und Daten aus einer Datenbank zieht. Beachten Sie, dass dies weder ein Beispiel für den "Produzenten" noch für den "Konsumenten" ist, sondern eher für einen Filter.

def ListAlbums(cursor):
    cursor.execute("SELECT title, artist FROM album")
    yield '<html><body><table><tr><td>Title</td><td>Artist</td></tr>'
    for (title, artist) in cursor:
        yield '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
    yield '</table></body></html>'

Das Problem besteht natürlich darin, dass die Datenbank eine Weile blockieren kann, bevor Zeilen zurückgegeben werden, und dass während der Ausführung Zeilen in Blöcken von 10 oder 100 auf einmal zurückgegeben werden können. Idealerweise, wenn die Datenbank für den nächsten Satz von Zeilen blockiert, könnte eine andere Benutzerverbindung bedient werden. Beachten Sie die vollständige Abwesenheit von SuspendIterator im obigen Code. Wenn es richtig gemacht wird, können sich Anwendungsentwickler auf die Funktionalität und nicht auf Nebenläufigkeitsprobleme konzentrieren.

Der durch den obigen Generator erzeugte Iterator sollte die notwendige Magie vollbringen, um den Zustand aufrechtzuerhalten und die Ausnahme an ein asynchrones Framework auf niedrigerer Ebene weiterzuleiten. Hier ist ein Beispiel dafür, wie der entsprechende Iterator aussehen würde, wenn er als Klasse codiert ist

class ListAlbums:

    def __init__(self, cursor):
        self.cursor = cursor

    def __iter__(self):
        self.cursor.execute("SELECT title, artist FROM album")
        self._iter = iter(self._cursor)
        self._next = self.state_head
        return self

    def next(self):
        return self._next()

    def state_head(self):
        self._next = self.state_cursor
        return "<html><body><table><tr><td>\
                Title</td><td>Artist</td></tr>"

    def state_tail(self):
        self._next = self.state_stop
        return "</table></body></html>"

    def state_cursor(self):
        try:
            (title,artist) = self._iter.next()
            return '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
        except StopIteration:
            self._next = self.state_tail
            return self.next()
        except SuspendIteration:
            # just pass-through
            raise

    def state_stop(self):
        raise StopIteration

Komplizierende Faktoren

Während das obige Beispiel einfach ist, wird es etwas komplizierter, wenn der Zwischengenerator Werte "kondensiert", d.h. er zieht zwei oder mehr Werte für jeden Wert, den er liefert. Zum Beispiel,

def pair(iterLeft,iterRight):
    rhs = iter(iterRight)
    lhs = iter(iterLeft)
    while True:
       yield (rhs.next(), lhs.next())

In diesem Fall muss das entsprechende Iterator-Verhalten subtiler sein, um den Fall zu behandeln, dass entweder der rechte oder der linke Iterator SuspendIteration auslöst. Es scheint eine Frage der Dekomposition des Generators zu sein, um Zwischenzustände zu erkennen, in denen eine SuspendIterator-Ausnahme aus dem erzeugenden Kontext auftreten könnte.

class pair:

    def __init__(self, iterLeft, iterRight):
        self.iterLeft = iterLeft
        self.iterRight = iterRight

    def __iter__(self):
        self.rhs = iter(iterRight)
        self.lhs = iter(iterLeft)
        self._temp_rhs = None
        self._temp_lhs = None
        self._next = self.state_rhs
        return self

    def next(self):
        return self._next()

    def state_rhs(self):
        self._temp_rhs = self.rhs.next()
        self._next = self.state_lhs
        return self.next()

    def state_lhs(self):
        self._temp_lhs = self.lhs.next()
        self._next = self.state_pair
        return self.next()

    def state_pair(self):
        self._next = self.state_rhs
        return (self._temp_rhs, self._temp_lhs)

Dieser Vorschlag geht davon aus, dass ein entsprechender Iterator, der mit dieser klassenbasierten Methode geschrieben wurde, für bestehende Generatoren möglich ist. Die Herausforderung scheint die Identifizierung von unterschiedlichen Zuständen innerhalb des Generators zu sein, in denen eine Aussetzung auftreten könnte.

Ressourcenbereinigung

Der aktuelle Generator-Mechanismus hat eine seltsame Interaktion mit Ausnahmen, bei der eine 'yield'-Anweisung nicht innerhalb eines try/finally-Blocks erlaubt ist. Die SuspendIterator-Ausnahme stellt ein weiteres ähnliches Problem dar. Die Auswirkungen dieses Problems sind nicht klar. Es könnte jedoch sein, dass das Umschreiben des Generators in eine Zustandsmaschine, wie im vorherigen Abschnitt geschehen, dieses Problem löst und die Situation nicht schlimmer macht, sondern vielleicht sogar die Yield/Finally-Situation beseitigt. Weitere Untersuchungen sind in diesem Bereich erforderlich.

API und Einschränkungen

Dieser Vorschlag deckt nur das "Aussetzen" einer Kette von Iteratoren ab und deckt (natürlich) nicht das Aussetzen allgemeiner Funktionen, Methoden oder C-Erweiterungsfunktionen ab. Während es keine direkte Unterstützung für die Erstellung von Generatoren in C-Code geben könnte, sind native C-Iteratoren, die mit der SuspendIterator-Semantik konform sind, sicherlich möglich.

Implementierung auf niedriger Ebene

Der Autor des PEP ist mit dem Python-Ausführungsmodell noch nicht vertraut, um sich in diesem Bereich zu äußern.

Referenzen


Source: https://github.com/python/peps/blob/main/peps/pep-0334.rst

Last modified: 2025-02-01 08:59:27 GMT