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 forSchleife in Python verwenden, gefolgt von einer Diskussion, um die Fakten zu veranschaulichen.
Fakten
Sie können einen Iterator von jedem Objekt abrufen, oindem Sie aufrufen, iter(o)ob mindestens eine der folgenden Bedingungen erfüllt ist:
a) ohat 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) ohat eine __getitem__Methode.
Es reicht nicht aus, nach einer Instanz von Iterableoder zu Sequencesuchen oder nach dem Attribut zu __iter__suchen.
Wenn ein Objekt onur implementiert __getitem__, aber nicht __iter__, iter(o)wird ein Iterator erstellt, der versucht, Elemente oanhand 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 StopIterationsich dann selbst aus.
Im allgemeinsten Sinne gibt es keine iterandere Möglichkeit, zu überprüfen, ob der von zurückgegebene Iterator vernünftig ist, als ihn auszuprobieren.
Wenn ein Objekt oimplementiert wird __iter__, stellt die iterFunktion 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 obeide __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 forSchleife in Python verwenden. Wenn Sie es bereits wissen, können Sie direkt zum nächsten Abschnitt springen.
Wenn Sie for item in ofü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 nextin 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 nextden Iterator auf, bis er ausgelöst StopIterationwird. 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 itermit einer Instanz von BasicIterablegibt einen Iterator ohne Probleme zurück, da BasicIterableimplementiert __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Es ist jedoch wichtig zu beachten, dass bdas __iter__Attribut nicht vorhanden ist und nicht als Instanz von Iterableoder 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, iterdas 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 xiterierbar ist, indem Sie iter(x)eine TypeErrorAusnahme 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 IterableABC nicht der Fall ist.
Zu Punkt 3: Iterieren über Objekte, die nur liefern __getitem__, aber nicht__iter__
Durchlaufen einer Instanz von BasicIterableArbeiten wie erwartet: Python erstellt einen Iterator, der versucht, Elemente anhand des Index abzurufen, beginnend bei Null, bis ein ausgelöst IndexErrorwird. Die __getitem__Methode des Demo-Objekts gibt einfach das zurück, itemwas __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, StopIterationwenn er das nächste Element nicht zurückgeben kann, und dass das IndexError, für item == 3das ausgelöst wird, intern behandelt wird. Aus diesem Grund funktioniert das Schleifen über a BasicIterablemit einer forSchleife 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 iterversucht, über den Index auf Elemente zuzugreifen. WrappedDicterbt 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, iterwird sichergestellt, dass der Rückgabewert von __iter__, wenn die Methode vorhanden ist, ein Iterator ist. Dies bedeutet, dass das zurückgegebene Objekt __next__(oder nextin Python 2) und implementieren muss __iter__. iterEs 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 FailIterIterableInstanzen sofort fehlschlägt, während das Erstellen eines Iterators aus Instanzen FailGetItemIterableerfolgreich 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__, iterwird 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 listImplementieren 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, iterschlä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