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

Python Enhancement Proposals

PEP 234 – Iteratoren

Autor:
Ka-Ping Yee <ping at zesty.ca>, Guido van Rossum <guido at python.org>
Status:
Final
Typ:
Standards Track
Erstellt:
30-Jan-2001
Python-Version:
2.1
Post-History:
30-Apr-2001

Inhaltsverzeichnis

Zusammenfassung

Dieses Dokument schlägt eine Iterator-Schnittstelle vor, die Objekte bereitstellen können, um das Verhalten von for-Schleifen zu steuern. Das Schleifen wird durch die Bereitstellung einer Methode angepasst, die ein Iterator-Objekt erzeugt. Der Iterator stellt eine Nächstes Element abrufen-Operation bereit, die jedes Mal, wenn sie aufgerufen wird, das nächste Element in der Sequenz liefert und eine Ausnahme auslöst, wenn keine weiteren Elemente verfügbar sind.

Zusätzlich werden spezifische Iteratoren über die Schlüssel eines Wörterbuchs und über die Zeilen einer Datei vorgeschlagen, und es wird vorgeschlagen, dict.has_key(key) als key in dict zu schreiben.

Hinweis: Dies ist eine fast vollständige Neufassung dieses PEP durch den zweiten Autor, die die tatsächliche Implementierung beschreibt, die in den Hauptzweig des Python 2.2 CVS-Baums eingecheckt wurde. Sie ist noch zur Diskussion offen. Einige der esoterischeren Vorschläge in der ursprünglichen Version dieses PEP wurden vorerst zurückgezogen; diese könnten Gegenstand eines separaten PEP in der Zukunft sein.

C API-Spezifikation

Eine neue Ausnahme wird definiert, StopIteration, die verwendet werden kann, um das Ende einer Iteration zu signalisieren.

Ein neuer Slot namens tp_iter zum Anfordern eines Iterators wird zur Typobjektstruktur hinzugefügt. Dies sollte eine Funktion mit einem Argument vom Typ PyObject * sein, die einen PyObject * zurückgibt, oder NULL. Um diesen Slot zu verwenden, wird eine neue C-API-Funktion PyObject_GetIter() mit derselben Signatur wie die Funktion des tp_iter-Slots hinzugefügt.

Ein weiterer neuer Slot, namens tp_iternext, wird zur Typstruktur hinzugefügt, um den nächsten Wert in der Iteration zu erhalten. Um diesen Slot zu verwenden, wird eine neue C-API-Funktion PyIter_Next() hinzugefügt. Die Signatur sowohl für den Slot als auch für die API-Funktion ist wie folgt, obwohl sich die Bedingungen für die Rückgabe von NULL unterscheiden: Das Argument ist ein PyObject * und ebenso der Rückgabewert. Wenn der Rückgabewert nicht NULL ist, ist es der nächste Wert in der Iteration. Wenn es NULL ist, dann gibt es für den tp_iternext Slot drei Möglichkeiten:

  • Es wird keine Ausnahme gesetzt; dies impliziert das Ende der Iteration.
  • Die Ausnahme StopIteration (oder eine abgeleitete Ausnahmeklasse) wird gesetzt; dies impliziert das Ende der Iteration.
  • Eine andere Ausnahme wird gesetzt; dies bedeutet, dass ein Fehler aufgetreten ist, der normal weitergegeben werden sollte.

Die höherwertige Funktion PyIter_Next() löscht die Ausnahme StopIteration (oder eine abgeleitete Ausnahme), wenn sie auftritt, sodass ihre NULL-Rückgabebedingungen einfacher sind:

  • Es wird keine Ausnahme gesetzt; dies bedeutet, dass die Iteration beendet ist.
  • Eine Ausnahme wird gesetzt; dies bedeutet, dass ein Fehler aufgetreten ist und normal weitergegeben werden sollte.

In C implementierte Iteratoren sollten keine Methode next() mit ähnlicher Semantik wie der tp_iternext-Slot implementieren! Wenn das Wörterbuch des Typs initialisiert wird (durch PyType_Ready()), bewirkt das Vorhandensein eines tp_iternext-Slots, dass eine Methode next(), die diesen Slot umschließt, zum tp_dict des Typs hinzugefügt wird. (Ausnahme: Wenn der Typ PyObject_GenericGetAttr() nicht zum Zugriff auf Instanzattribute verwendet, wird die Methode next() im tp_dict des Typs möglicherweise nicht gesehen.) (Aufgrund eines Missverständnisses im ursprünglichen Text dieses PEP implementierten in Python 2.2 alle Iteratortypen eine Methode next(), die durch den Wrapper überschrieben wurde; dies wurde in Python 2.3 behoben.)

Um die binäre Rückwärtskompatibilität zu gewährleisten, wird ein neues Flag Py_TPFLAGS_HAVE_ITER zur Menge der Flags im Feld tp_flags und zum Standard-Flags-Makro hinzugefügt. Dieses Flag muss getestet werden, bevor auf die tp_iter- oder tp_iternext-Slots zugegriffen wird. Das Makro PyIter_Check() prüft, ob ein Objekt das entsprechende Flag gesetzt hat und über einen nicht NULL tp_iternext-Slot verfügt. Es gibt kein solches Makro für den tp_iter-Slot (da die einzige Stelle, an der auf diesen Slot verwiesen wird, PyObject_GetIter() sein sollte, und dies kann direkt auf das Flag Py_TPFLAGS_HAVE_ITER geprüft werden).

(Hinweis: Der tp_iter-Slot kann auf jedem Objekt vorhanden sein; der tp_iternext-Slot sollte nur auf Objekten vorhanden sein, die als Iteratoren fungieren.)

Für Rückwärtskompatibilität implementiert die Funktion PyObject_GetIter() Fallback-Semantik, wenn ihr Argument eine Sequenz ist, die keine tp_iter-Funktion implementiert: In diesem Fall wird ein leichtgewichtiger Sequenz-Iterator-Objekt erstellt, das über die Elemente der Sequenz in natürlicher Reihenfolge iteriert.

Der für for-Schleifen generierte Python-Bytecode wird geändert, um neue Opcodes zu verwenden, GET_ITER und FOR_ITER, die das Iterator-Protokoll anstelle des Sequenz-Protokolls verwenden, um den nächsten Wert für die Schleifenvariable zu erhalten. Dies macht es möglich, eine for-Schleife zu verwenden, um über Nicht-Sequenz-Objekte zu iterieren, die den tp_iter-Slot unterstützen. Andere Stellen, an denen der Interpreter über die Werte einer Sequenz schleift, sollten ebenfalls geändert werden, um Iteratoren zu verwenden.

Iteratoren sollten den tp_iter-Slot so implementieren, dass er eine Referenz auf sich selbst zurückgibt. Dies ist notwendig, damit ein Iterator (im Gegensatz zu einer Sequenz) in einer for-Schleife verwendet werden kann.

Iterator-Implementierungen (in C oder Python) sollten sicherstellen, dass, sobald der Iterator seine Erschöpfung signalisiert hat, nachfolgende Aufrufe von tp_iternext oder der Methode next() dies weiterhin tun. Es ist nicht spezifiziert, ob ein Iterator in den erschöpften Zustand übergeht, wenn eine Ausnahme (außer StopIteration) ausgelöst wird. Beachten Sie, dass Python nicht garantieren kann, dass benutzerdefinierte oder 3rd-Party-Iteratoren diese Anforderung korrekt implementieren.

Python API-Spezifikation

Die Ausnahme StopIteration wird als eine der Standardausnahmen sichtbar gemacht. Sie wird von Exception abgeleitet.

Eine neue integrierte Funktion wird definiert, iter(), die auf zwei Arten aufgerufen werden kann:

  • iter(obj) ruft PyObject_GetIter(obj) auf.
  • iter(callable, sentinel) gibt eine spezielle Art von Iterator zurück, die den Aufrufer aufruft, um einen neuen Wert zu erzeugen, und den Rückgabewert mit dem Sentinel-Wert vergleicht. Wenn der Rückgabewert dem Sentinel entspricht, signalisiert dies das Ende der Iteration und StopIteration wird ausgelöst, anstatt normal zurückzukehren; wenn der Rückgabewert nicht dem Sentinel entspricht, wird er als nächster Wert vom Iterator zurückgegeben. Wenn der Aufrufer eine Ausnahme auslöst, wird diese normal weitergegeben; insbesondere darf die Funktion StopIteration als alternative Möglichkeit zum Beenden der Iteration auslösen. (Diese Funktionalität ist über die C-API als PyCallIter_New(callable, sentinel) verfügbar.)

Iterator-Objekte, die von einer der beiden Formen von iter() zurückgegeben werden, haben eine Methode next(). Diese Methode gibt entweder den nächsten Wert in der Iteration zurück oder löst StopIteration (oder eine abgeleitete Ausnahmeklasse) aus, um das Ende der Iteration zu signalisieren. Jede andere Ausnahme sollte als Fehler betrachtet und normal weitergegeben werden, nicht als Ende der Iteration.

Klassen können definieren, wie sie iteriert werden, indem sie eine Methode __iter__() definieren. Diese sollte keine zusätzlichen Argumente entgegennehmen und ein gültiges Iterator-Objekt zurückgeben. Eine Klasse, die ein Iterator sein möchte, sollte zwei Methoden implementieren: eine Methode next(), die sich wie oben beschrieben verhält, und eine Methode __iter__(), die self zurückgibt.

Die beiden Methoden entsprechen zwei unterschiedlichen Protokollen:

  1. Ein Objekt kann mit for iteriert werden, wenn es __iter__() oder __getitem__() implementiert.
  2. Ein Objekt kann als Iterator fungieren, wenn es next() implementiert.

Container-ähnliche Objekte unterstützen normalerweise Protokoll 1. Iteratoren sind derzeit verpflichtet, beide Protokolle zu unterstützen. Die Semantik der Iteration stammt nur aus Protokoll 2; Protokoll 1 ist vorhanden, damit Iteratoren sich wie Sequenzen verhalten; insbesondere damit Code, der einen Iterator empfängt, eine for-Schleife über den Iterator verwenden kann.

Dictionary-Iteratoren

  • Wörterbücher implementieren einen sq_contains-Slot, der denselben Test implementiert wie die Methode has_key(). Dies bedeutet, dass wir schreiben können:
    if k in dict: ...
    

    was äquivalent ist zu

    if dict.has_key(k): ...
    
  • Wörterbücher implementieren einen tp_iter-Slot, der einen effizienten Iterator zurückgibt, der über die Schlüssel des Wörterbuchs iteriert. Während einer solchen Iteration sollte das Wörterbuch nicht modifiziert werden, mit Ausnahme der Zuweisung eines Wertes für einen vorhandenen Schlüssel (Löschungen oder Hinzufügungen nicht, ebenso wenig die Methode update()). Das bedeutet, dass wir schreiben können:
    for k in dict: ...
    

    was äquivalent, aber viel schneller ist als

    for k in dict.keys(): ...
    

    solange die Einschränkungen bei der Modifikation des Wörterbuchs (entweder durch die Schleife oder durch einen anderen Thread) nicht verletzt werden.

  • Fügen Sie Methoden zu Wörterbüchern hinzu, die explizit verschiedene Arten von Iteratoren zurückgeben.
    for key in dict.iterkeys(): ...
    
    for value in dict.itervalues(): ...
    
    for key, value in dict.iteritems(): ...
    

    Dies bedeutet, dass for x in dict eine Kurzform für for x in dict.iterkeys() ist.

Andere Mappings, wenn sie Iteratoren überhaupt unterstützen, sollten ebenfalls über die Schlüssel iterieren. Dies sollte jedoch keine absolute Regel sein; spezifische Anwendungen können unterschiedliche Anforderungen haben.

Datei-Iteratoren

Der folgende Vorschlag ist nützlich, da er uns eine gute Antwort auf die Beschwerde gibt, dass das gängige Idiom zum Iterieren über die Zeilen einer Datei hässlich und langsam ist.

  • Dateien implementieren einen tp_iter-Slot, der äquivalent zu iter(f.readline, "") ist. Das bedeutet, dass wir schreiben können:
    for line in file:
        ...
    

    als Kurzform für

    for line in iter(file.readline, ""):
        ...
    

    was äquivalent, aber schneller ist als

    while 1:
        line = file.readline()
        if not line:
            break
        ...
    

Dies zeigt auch, dass einige Iteratoren destruktiv sind: Sie verbrauchen alle Werte, und ein zweiter Iterator kann nicht einfach erstellt werden, um unabhängig über dieselben Werte zu iterieren. Man könnte die Datei ein zweites Mal öffnen oder mit seek() zum Anfang springen, aber diese Lösungen funktionieren nicht für alle Dateitypen, z. B. nicht, wenn das geöffnete Dateiobjekt tatsächlich eine Pipe oder einen Stream-Socket repräsentiert.

Da der Datei-Iterator einen internen Puffer verwendet, funktioniert die Vermischung mit anderen Dateioperationen (z. B. file.readline()) nicht richtig. Auch der folgende Code:

for line in file:
    if line == "\n":
        break
for line in file:
   print line,

funktioniert nicht wie erwartet, da der Iterator, der von der zweiten for-Schleife erstellt wird, den von der ersten for-Schleife gelesenen Puffer nicht berücksichtigt. Eine korrekte Schreibweise dafür ist:

it = iter(file)
for line in it:
    if line == "\n":
        break
for line in it:
    print line,

(Die Begründung für diese Einschränkungen ist, dass for line in file die empfohlene, standardmäßige Art und Weise sein sollte, über die Zeilen einer Datei zu iterieren, und dies so schnell wie möglich sein sollte. Die Iterator-Version ist erheblich schneller als der Aufruf von readline(), aufgrund des internen Puffers im Iterator.)

Begründung

Wenn alle Teile des Vorschlags enthalten sind, werden viele Bedenken auf konsistente und flexible Weise adressiert. Zu seinen Hauptvorzügen gehören die folgenden vier – nein, fünf – nein, sechs – Punkte:

  1. Es bietet eine erweiterbare Iterator-Schnittstelle.
  2. Es ermöglicht Leistungsverbesserungen bei der Listeniteration.
  3. Es ermöglicht große Leistungsverbesserungen bei der Wörterbuchiteration.
  4. Es ermöglicht die Bereitstellung einer Schnittstelle nur für die Iteration, ohne den Anspruch auf zufälligen Zugriff auf Elemente zu erwecken.
  5. Es ist rückwärtskompatibel mit allen bestehenden benutzerdefinierten Klassen und Erweiterungsobjekten, die Sequenzen und Mappings emulieren, selbst mit Mappings, die nur einen Teil von {__getitem__, keys, values, items} implementieren.
  6. Es macht Code, der über Nicht-Sequenz-Sammlungen iteriert, prägnanter und lesbarer.

Gelöste Probleme

Die folgenden Themen wurden durch Konsens oder BDFL-Äußerung entschieden.

  • Zwei alternative Schreibweisen für next() wurden vorgeschlagen, aber abgelehnt: __next__(), da sie einem Typobjekt-Slot entspricht (tp_iternext); und __call__(), da dies die einzige Operation ist.

    Argumente gegen __next__(): Obwohl viele Iteratoren in for-Schleifen verwendet werden, wird erwartet, dass Benutzercode auch next() direkt aufruft, sodass das Schreiben von __next__() hässlich ist; außerdem wäre eine mögliche Erweiterung des Protokolls die Zulassung von prev(), current() und reset()-Operationen; sicher wollen wir nicht __prev__(), __current__(), __reset__() verwenden.

    Argumente gegen __call__() (der ursprüngliche Vorschlag): Aus dem Kontext gerissen ist x() nicht sehr lesbar, während x.next() klar ist; es besteht die Gefahr, dass jedes spezielle Objekt __call__() für seine häufigste Operation verwenden möchte, was mehr Verwirrung als Klarheit stiftet.

    (Rückblickend wäre es vielleicht besser gewesen, __next__() zu verwenden und eine neue integrierte Funktion, next(it), die it.__next__() aufruft. Aber leider ist es zu spät; dies wurde seit Dezember 2001 in Python 2.2 bereitgestellt.)

  • Einige Leute haben die Möglichkeit angefordert, einen Iterator neu zu starten. Dies sollte durch wiederholtes Aufrufen von iter() für eine Sequenz erfolgen, nicht durch das Iterator-Protokoll selbst. (Siehe auch angeforderte Erweiterungen unten.)
  • Es wurde angezweifelt, ob eine Ausnahme zur Signalisierung des Endes der Iteration zu teuer ist. Mehrere Alternativen zur Ausnahme StopIteration wurden vorgeschlagen: ein spezieller Wert End zur Signalisierung des Endes, eine Funktion end(), um zu prüfen, ob der Iterator fertig ist, sogar die Wiederverwendung der Ausnahme IndexError.
    • Ein spezieller Wert hat das Problem, dass, wenn eine Sequenz diesen speziellen Wert jemals enthält, eine Schleife über diese Sequenz vorzeitig und ohne Vorwarnung endet. Wenn uns die Erfahrung mit Null-terminierten C-Strings die Probleme damit nicht gelehrt hat, stellen Sie sich den Ärger vor, den ein Python-Introspektionstool beim Iterieren über eine Liste aller integrierten Namen hätte, unter der Annahme, dass der spezielle End-Wert ein integrierter Name wäre!
    • Das Aufrufen einer end()-Funktion würde zwei Aufrufe pro Iteration erfordern. Zwei Aufrufe sind wesentlich teurer als ein Aufruf plus eine Prüfung auf eine Ausnahme. Insbesondere die zeitkritische for-Schleife kann sehr günstig auf eine Ausnahme prüfen.
    • Die Wiederverwendung von IndexError kann zu Verwirrung führen, da es sich um einen echten Fehler handeln kann, der durch ein vorzeitiges Beenden der Schleife maskiert würde.
  • Einige haben nach einem standardmäßigen Iteratortyp gefragt. Vermutlich müssten alle Iteratoren von diesem Typ abgeleitet sein. Aber das ist nicht der Python-Weg: Wörterbücher sind Mappings, weil sie __getitem__() und eine Handvoll anderer Operationen unterstützen, nicht weil sie von einem abstrakten Mapping-Typ abgeleitet sind.
  • Bezüglich if key in dict: Es besteht kein Zweifel, dass die Interpretation dict.has_key(x) von x in dict die bei weitem nützlichste Interpretation ist, wahrscheinlich die einzig nützliche. Es gab Widerstand dagegen, weil x in list prüft, ob x in den Werten vorhanden ist, während der Vorschlag x in dict prüfen lässt, ob x in den Schlüsseln vorhanden ist. Da die Symmetrie zwischen Listen und Wörterbüchern sehr schwach ist, hat dieses Argument kein großes Gewicht.
  • Der Name iter() ist eine Abkürzung. Vorgeschlagene Alternativen waren iterate(), traverse(), aber diese erscheinen zu lang. Python hat eine Geschichte der Verwendung von Abkürzungen für gängige integrierte Funktionen, z. B. repr(), str(), len().

    Entscheidung: iter() ist es.

  • Die Verwendung desselben Namens für zwei verschiedene Operationen (Erhalten eines Iterators aus einem Objekt und Erstellen eines Iterators für eine Funktion mit einem Sentinel-Wert) ist etwas hässlich. Ich habe jedoch keinen besseren Namen für die zweite Operation gesehen, und da beide einen Iterator zurückgeben, ist er leicht zu merken.

    Entscheidung: Die integrierte Funktion iter() nimmt ein optionales Argument entgegen, das das zu suchende Sentinel ist.

  • Wird ein bestimmtes Iterator-Objekt einmal StopIteration ausgelöst haben, wird es dann auch bei allen nachfolgenden Aufrufen von next() StopIteration auslösen? Einige sagen, es wäre nützlich, dies zu verlangen, andere sagen, es sei nützlich, dies den einzelnen Iteratoren zu überlassen. Beachten Sie, dass dies für einige Iterator-Implementierungen möglicherweise ein zusätzliches Zustandsbit erfordert (z. B. Funktions-Wrapper-Iteratoren).

    Entscheidung: Sobald StopIteration ausgelöst wird, löst der Aufruf von it.next() weiterhin StopIteration aus.

    Hinweis: Dies wurde tatsächlich nicht in Python 2.2 implementiert; es gibt viele Fälle, in denen die Methode next() eines Iterators bei einem Aufruf StopIteration auslösen kann, beim nächsten aber nicht. Dies wurde in Python 2.3 behoben.

  • Es wurde vorgeschlagen, dass ein Datei-Objekt sein eigener Iterator sein sollte, mit einer Methode next(), die die nächste Zeile zurückgibt. Dies hat bestimmte Vorteile und macht deutlich, dass dieser Iterator destruktiv ist. Der Nachteil ist, dass es die Implementierung des "sticky StopIteration"-Features, das im vorherigen Punkt vorgeschlagen wurde, noch schmerzhafter machen würde.

    Entscheidung: Vorläufig abgelehnt (obwohl immer noch Leute dafür argumentieren).

  • Einige Leute haben nach Erweiterungen des Iterator-Protokolls gefragt, z. B. prev(), um das vorherige Element zu erhalten, current(), um das aktuelle Element erneut zu erhalten, finished(), um zu prüfen, ob der Iterator fertig ist, und vielleicht noch andere, wie rewind(), __len__(), position().

    Während einige davon nützlich sind, können viele davon nicht einfach für alle Iteratortypen implementiert werden, ohne willkürliches Buffering hinzuzufügen, und manchmal können sie gar nicht (oder nicht vernünftig) implementiert werden. Z. B. kann alles, was mit umgekehrten Richtungen zu tun hat, nicht beim Iterieren über eine Datei oder Funktion erfolgen. Vielleicht kann ein separater PEP entworfen werden, um die Namen für solche Operationen zu standardisieren, wenn sie implementierbar sind.

    Entscheidung: abgelehnt.

  • Es gab eine lange Diskussion darüber, ob
    for x in dict: ...
    

    x die aufeinanderfolgenden Schlüssel, Werte oder Elemente des Wörterbuchs zugewiesen werden sollte. Die Symmetrie zwischen if x in y und for x in y legt nahe, dass über Schlüssel iteriert werden sollte. Diese Symmetrie wurde von vielen unabhängig voneinander beobachtet und sogar dazu verwendet, die eine mit der anderen zu "erklären". Dies liegt daran, dass für Sequenzen if x in y über y iteriert und die iterierten Werte mit x vergleicht. Wenn wir beide oben genannten Vorschläge annehmen, wird dies auch für Wörterbücher gelten.

    Das Argument gegen die Tatsache, dass for x in dict über die Schlüssel iteriert, stammt hauptsächlich aus praktischer Sicht: Scans der Standardbibliothek zeigen, dass es ungefähr so viele Verwendungen von for x in dict.items() wie von for x in dict.keys() gibt, wobei die items()-Version eine leichte Mehrheit hat. Vermutlich verwenden viele der Schleifen, die keys() verwenden, ohnehin den entsprechenden Wert, indem sie dict[x] schreiben, sodass (so das Argument) durch Bereitstellung von Schlüssel und Wert beide verfügbar wären, wir die größte Anzahl von Fällen unterstützen könnten. Obwohl das wahr ist, finde ich (Guido) die Entsprechung zwischen for x in dict und if x in dict zu zwingend, um sie zu brechen, und es ist kein großer Overhead, dict[x] schreiben zu müssen, um explizit den Wert zu erhalten.

    Für schnelle Iteration über Elemente verwenden Sie for key, value in dict.iteritems(). Ich habe den Unterschied zwischen

    for key in dict: dict[key]
    

    und

    for key, value in dict.iteritems(): pass
    

    und habe festgestellt, dass letzteres nur etwa 7 % schneller ist.

    Entscheidung: Durch BDFL-Entscheidung iteriert for x in dict über die Schlüssel, und Wörterbücher haben iteritems(), iterkeys() und itervalues(), um die verschiedenen Varianten von Wörterbuch-Iteratoren zurückzugeben.

Mailinglisten

Das Iterator-Protokoll wurde ausführlich in einer Mailingliste auf SourceForge diskutiert

Anfänglich wurde ein Teil der Diskussion bei Yahoo geführt; Archive sind weiterhin zugänglich.


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT