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
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)ruftPyObject_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 undStopIterationwird 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 FunktionStopIterationals alternative Möglichkeit zum Beenden der Iteration auslösen. (Diese Funktionalität ist über die C-API alsPyCallIter_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:
- Ein Objekt kann mit
foriteriert werden, wenn es__iter__()oder__getitem__()implementiert. - 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 Methodehas_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 Methodeupdate()). 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 dicteine Kurzform fürfor 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 zuiter(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:
- Es bietet eine erweiterbare Iterator-Schnittstelle.
- Es ermöglicht Leistungsverbesserungen bei der Listeniteration.
- Es ermöglicht große Leistungsverbesserungen bei der Wörterbuchiteration.
- Es ermöglicht die Bereitstellung einer Schnittstelle nur für die Iteration, ohne den Anspruch auf zufälligen Zugriff auf Elemente zu erwecken.
- 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. - 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 auchnext()direkt aufruft, sodass das Schreiben von__next__()hässlich ist; außerdem wäre eine mögliche Erweiterung des Protokolls die Zulassung vonprev(),current()undreset()-Operationen; sicher wollen wir nicht__prev__(),__current__(),__reset__()verwenden.Argumente gegen
__call__()(der ursprüngliche Vorschlag): Aus dem Kontext gerissen istx()nicht sehr lesbar, währendx.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), dieit.__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
StopIterationwurden vorgeschlagen: ein spezieller WertEndzur Signalisierung des Endes, eine Funktionend(), um zu prüfen, ob der Iterator fertig ist, sogar die Wiederverwendung der AusnahmeIndexError.- 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
IndexErrorkann zu Verwirrung führen, da es sich um einen echten Fehler handeln kann, der durch ein vorzeitiges Beenden der Schleife maskiert würde.
- 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
- 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 Interpretationdict.has_key(x)vonx in dictdie bei weitem nützlichste Interpretation ist, wahrscheinlich die einzig nützliche. Es gab Widerstand dagegen, weilx in listprüft, ob x in den Werten vorhanden ist, während der Vorschlagx in dictprü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 wareniterate(),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
StopIterationausgelöst haben, wird es dann auch bei allen nachfolgenden Aufrufen vonnext()StopIterationauslö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
StopIterationausgelöst wird, löst der Aufruf vonit.next()weiterhinStopIterationaus.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 AufrufStopIterationauslö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, wierewind(),__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 yundfor x in ylegt 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 Sequenzenif 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 vonfor x in dict.items()wie vonfor x in dict.keys()gibt, wobei dieitems()-Version eine leichte Mehrheit hat. Vermutlich verwenden viele der Schleifen, diekeys()verwenden, ohnehin den entsprechenden Wert, indem siedict[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 zwischenfor x in dictundif x in dictzu 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 zwischenfor 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 habeniteritems(),iterkeys()unditervalues(), 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.
Urheberrecht
Dieses Dokument ist gemeinfrei.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0234.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT