Ich möchte ein wenig mehr Licht biss sich auf das Zusammenspiel von iter
, __iter__
und , __getitem__
und was hinter den Kulissen passiert. Mit diesem Wissen können Sie verstehen, warum das Beste, was Sie tun können, ist
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Ich werde zuerst die Fakten auflisten und anschließend kurz daran erinnern, was passiert, wenn Sie eine for
Schleife in Python verwenden, gefolgt von einer Diskussion, um die Fakten zu veranschaulichen.
Fakten
Sie können einen Iterator von jedem Objekt abrufen, o
indem Sie aufrufen, iter(o)
ob mindestens eine der folgenden Bedingungen erfüllt ist:
a) o
hat eine __iter__
Methode, die ein Iteratorobjekt zurückgibt. Ein Iterator ist ein beliebiges Objekt mit einer __iter__
und einer __next__
(Python 2 :) next
-Methode.
b) o
hat eine __getitem__
Methode.
Es reicht nicht aus, nach einer Instanz von Iterable
oder zu Sequence
suchen oder nach dem Attribut zu __iter__
suchen.
Wenn ein Objekt o
nur implementiert __getitem__
, aber nicht __iter__
, iter(o)
wird ein Iterator erstellt, der versucht, Elemente o
anhand eines ganzzahligen Index ab Index 0 abzurufen . Der Iterator fängt alle IndexError
(aber keine anderen Fehler) ab, die ausgelöst werden, und löst StopIteration
sich dann selbst aus.
Im allgemeinsten Sinne gibt es keine iter
andere Möglichkeit, zu überprüfen, ob der von zurückgegebene Iterator vernünftig ist, als ihn auszuprobieren.
Wenn ein Objekt o
implementiert wird __iter__
, stellt die iter
Funktion sicher, dass das von zurückgegebene Objekt __iter__
ein Iterator ist. Es gibt keine Überprüfung der Integrität, wenn ein Objekt nur implementiert wird __getitem__
.
__iter__
Gewinnt. Wenn ein Objekt o
beide __iter__
und implementiert __getitem__
, iter(o)
wird aufgerufen __iter__
.
Wenn Sie Ihre eigenen Objekte iterierbar machen möchten, implementieren Sie immer die __iter__
Methode.
for
Schleifen
Um mitzumachen, müssen Sie verstehen, was passiert, wenn Sie eine for
Schleife in Python verwenden. Wenn Sie es bereits wissen, können Sie direkt zum nächsten Abschnitt springen.
Wenn Sie for item in o
für ein iterierbares Objekt verwenden o
, ruft Python iter(o)
ein Iteratorobjekt auf und erwartet es als Rückgabewert. Ein Iterator ist ein beliebiges Objekt, das eine __next__
(oder next
in Python 2) Methode und eine __iter__
Methode implementiert .
Konventionell sollte die __iter__
Methode eines Iterators das Objekt selbst zurückgeben (dh return self
). Python ruft dann next
den Iterator auf, bis er ausgelöst StopIteration
wird. All dies geschieht implizit, aber die folgende Demonstration macht es sichtbar:
import random
class DemoIterable(object):
def __iter__(self):
print('__iter__ called')
return DemoIterator()
class DemoIterator(object):
def __iter__(self):
return self
def __next__(self):
print('__next__ called')
r = random.randint(1, 10)
if r == 5:
print('raising StopIteration')
raise StopIteration
return r
Iteration über a DemoIterable
:
>>> di = DemoIterable()
>>> for x in di:
... print(x)
...
__iter__ called
__next__ called
9
__next__ called
8
__next__ called
10
__next__ called
3
__next__ called
10
__next__ called
raising StopIteration
Diskussion und Illustrationen
Zu Punkt 1 und 2: Erhalten eines Iterators und unzuverlässiger Überprüfungen
Betrachten Sie die folgende Klasse:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Das Aufrufen iter
mit einer Instanz von BasicIterable
gibt einen Iterator ohne Probleme zurück, da BasicIterable
implementiert __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Es ist jedoch wichtig zu beachten, dass b
das __iter__
Attribut nicht vorhanden ist und nicht als Instanz von Iterable
oder betrachtet wird Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Aus diesem Grund empfiehlt Fluent Python von Luciano Ramalho, iter
das Potenzial aufzurufen und zu behandeln TypeError
, um zu überprüfen, ob ein Objekt iterierbar ist. Zitat direkt aus dem Buch:
Ab Python 3.4 können Sie am genauesten überprüfen, ob ein Objekt x
iterierbar ist, indem Sie iter(x)
eine TypeError
Ausnahme aufrufen und behandeln, wenn dies nicht der Fall ist. Dies ist genauer als die Verwendung isinstance(x, abc.Iterable)
, da iter(x)
auch die Legacy- __getitem__
Methode berücksichtigt wird, während dies beim Iterable
ABC nicht der Fall ist.
Zu Punkt 3: Iterieren über Objekte, die nur liefern __getitem__
, aber nicht__iter__
Durchlaufen einer Instanz von BasicIterable
Arbeiten wie erwartet: Python erstellt einen Iterator, der versucht, Elemente anhand des Index abzurufen, beginnend bei Null, bis ein ausgelöst IndexError
wird. Die __getitem__
Methode des Demo-Objekts gibt einfach das zurück, item
was __getitem__(self, item)
vom Iterator, der von zurückgegeben wurde, als Argument angegeben wurde iter
.
>>> b = BasicIterable()
>>> it = iter(b)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Beachten Sie, dass der Iterator ausgelöst wird, StopIteration
wenn er das nächste Element nicht zurückgeben kann, und dass das IndexError
, für item == 3
das ausgelöst wird, intern behandelt wird. Aus diesem Grund funktioniert das Schleifen über a BasicIterable
mit einer for
Schleife wie erwartet:
>>> for x in b:
... print(x)
...
0
1
2
Hier ist ein weiteres Beispiel, um das Konzept zu verdeutlichen, wie der von zurückgegebene Iterator iter
versucht, über den Index auf Elemente zuzugreifen. WrappedDict
erbt nicht von dict
, was bedeutet, dass Instanzen keine __iter__
Methode haben.
class WrappedDict(object): # note: no inheritance from dict!
def __init__(self, dic):
self._dict = dic
def __getitem__(self, item):
try:
return self._dict[item] # delegate to dict.__getitem__
except KeyError:
raise IndexError
Beachten Sie, dass Aufrufe an __getitem__
delegiert werden, dict.__getitem__
für die die eckige Klammer nur eine Abkürzung ist.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
Zu Punkt 4 und 5: iter
Überprüft beim Aufrufen nach einem Iterator__iter__
:
Wenn iter(o)
für ein Objekt aufgerufen wird o
, iter
wird sichergestellt, dass der Rückgabewert von __iter__
, wenn die Methode vorhanden ist, ein Iterator ist. Dies bedeutet, dass das zurückgegebene Objekt __next__
(oder next
in Python 2) und implementieren muss __iter__
. iter
Es können keine Integritätsprüfungen für Objekte durchgeführt werden, die nur bereitstellen __getitem__
, da nicht überprüft werden kann, ob auf die Elemente des Objekts über einen Ganzzahlindex zugegriffen werden kann.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Beachten Sie, dass das Erstellen eines Iterators aus FailIterIterable
Instanzen sofort fehlschlägt, während das Erstellen eines Iterators aus Instanzen FailGetItemIterable
erfolgreich ist, aber beim ersten Aufruf von eine Ausnahme auslöst __next__
.
>>> fii = FailIterIterable()
>>> iter(fii)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'object'
>>>
>>> fgi = FailGetitemIterable()
>>> it = iter(fgi)
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/path/iterdemo.py", line 42, in __getitem__
raise Exception
Exception
Zu Punkt 6: __iter__
gewinnt
Dieser ist unkompliziert. Wenn ein Objekt implementiert __iter__
und __getitem__
, iter
wird aufgerufen __iter__
. Betrachten Sie die folgende Klasse
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
und die Ausgabe beim Durchlaufen einer Instanz:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
Zu Punkt 7: Ihre iterierbaren Klassen sollten implementiert werden __iter__
Sie könnten sich fragen, warum die meisten eingebauten Sequenzen wie das list
Implementieren einer __iter__
Methode __getitem__
ausreichen würden.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Immerhin Iteration über Instanzen der Klasse oben, die Delegierten Anrufe __getitem__
an list.__getitem__
(die eckige Klammer - Notation), werden gut funktionieren:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Die Gründe, die Ihre benutzerdefinierten Iterables implementieren sollten, __iter__
sind folgende:
- Wenn Sie implementieren
__iter__
, werden Instanzen als iterabel betrachtet und isinstance(o, collections.abc.Iterable)
zurückgegeben True
.
- Wenn das von zurückgegebene Objekt
__iter__
kein Iterator ist, iter
schlägt dies sofort fehl und löst a aus TypeError
.
- Die spezielle Behandlung von
__getitem__
besteht aus Gründen der Abwärtskompatibilität. Nochmals aus Fluent Python zitieren:
Deshalb ist jede Python-Sequenz iterierbar: Sie alle implementieren __getitem__
. Tatsächlich werden auch die Standardsequenzen implementiert __iter__
, und Ihre sollten dies auch tun, da die spezielle Behandlung von __getitem__
aus Gründen der Abwärtskompatibilität besteht und möglicherweise in Zukunft weg sein wird (obwohl sie beim Schreiben nicht veraltet ist).
__getitem__
ist auch ausreichend, um ein Objekt iterierbar zu machen