Chciałbym rzucić nieco więcej światła na grę iter, __iter__a __getitem__i co dzieje się za kurtyną. Uzbrojony w tę wiedzę będziesz w stanie zrozumieć, dlaczego najlepiej możesz to zrobić
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Najpierw wymienię fakty, a następnie szybko przypomnę, co się stanie, gdy zastosujesz forpętlę w pythonie, a następnie przeprowadzimy dyskusję ilustrującą fakty.
Fakty
Możesz pobrać iterator z dowolnego obiektu o, wywołując, iter(o)jeśli spełniony jest przynajmniej jeden z następujących warunków:
a) oma __iter__metodę, która zwraca obiekt iteratora. Iteratorem jest dowolny obiekt z metodą __iter__i __next__(Python 2 next:).
b) oma __getitem__metodę.
Sprawdzanie instancji Iterablelub Sequencelub kontroli dla atrybutu __iter__nie jest wystarczające.
Jeśli obiekt oimplementuje tylko __getitem__, ale nie __iter__, iter(o)skonstruuje iterator, który spróbuje pobrać elementy z oindeksu liczb całkowitych, zaczynając od indeksu 0. Iterator wyłapie wszystkie IndexError(ale żadnych innych błędów), które zostaną podniesione, a następnie StopIterationsam się podniesie .
W najogólniejszym sensie nie ma sposobu, aby sprawdzić, czy powracający iterator iterjest zdrowy, poza wypróbowaniem go.
Jeśli obiekt zostanie ozaimplementowany __iter__, iterfunkcja upewni się, że zwracany przez niego obiekt __iter__jest iteratorem. Nie ma kontroli poprawności, jeśli obiekt tylko implementuje __getitem__.
__iter__wygrywa Jeśli obiekt oimplementuje oba __iter__i __getitem__, iter(o)wywoła __iter__.
Jeśli chcesz, aby własne obiekty były iterowalne, zawsze implementuj __iter__metodę.
for pętle
Aby kontynuować, musisz zrozumieć, co się dzieje, gdy zastosujesz forpętlę w Pythonie. Jeśli już wiesz, możesz przejść do następnej sekcji.
Kiedy używasz for item in odla jakiegoś iterowalnego obiektu o, Python wywołuje iter(o)i oczekuje obiektu iteratora jako wartości zwracanej. Iterator to dowolny obiekt, który implementuje metodę __next__(lub nextw Pythonie 2) i __iter__metodę.
Zgodnie z konwencją __iter__metoda iteratora powinna zwrócić sam obiekt (tj return self.). Python następnie wywołuje nextiterator, aż StopIterationzostanie podniesiony. Wszystko to dzieje się niejawnie, ale poniższa demonstracja pokazuje:
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
Iteracja po 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
Dyskusja i ilustracje
W punktach 1 i 2: uzyskanie iteratora i nierzetelne kontrole
Rozważ następującą klasę:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Wywołanie iterz instancją BasicIterablezwróci iterator bez żadnych problemów, ponieważ BasicIterableimplementuje __getitem__.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Należy jednak zauważyć, że bnie ma tego __iter__atrybutu i nie jest uważany za instancję Iterablelub Sequence:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Właśnie dlatego Fluent Python autorstwa Luciano Ramalho zaleca wywoływanie iteri zarządzanie potencjałem TypeErrorjako najdokładniejszy sposób sprawdzenia, czy obiekt jest iterowalny. Cytując bezpośrednio z książki:
Od wersji Python 3.4 najdokładniejszym sposobem sprawdzenia, czy obiekt xjest iterowalny, jest wywołanie iter(x)i obsłużenie TypeErrorwyjątku, jeśli tak nie jest. Jest to dokładniejsze niż używanie isinstance(x, abc.Iterable), ponieważ iter(x)uwzględnia również starszą __getitem__metodę, podczas gdy IterableABC nie.
W punkcie 3: Iterowanie nad obiektami, które tylko zapewniają __getitem__, ale nie zapewniają__iter__
Iteracja po instancji BasicIterabledziała zgodnie z oczekiwaniami: Python konstruuje iterator, który próbuje pobierać elementy według indeksu, zaczynając od zera, aż do IndexErrorpodniesienia wartości. Metoda obiektu demonstracyjnego __getitem__zwraca po prostu to, itemco podano jako argument __getitem__(self, item)zwracany przez iterator 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
Zauważ, że iterator podnosi się, StopIterationgdy nie może zwrócić następnego elementu, a ten, dla IndexErrorktórego podniesiono, item == 3jest obsługiwany wewnętrznie. Oto dlaczego zapętlenie BasicIterablez forpętlą działa zgodnie z oczekiwaniami:
>>> for x in b:
... print(x)
...
0
1
2
Oto kolejny przykład, który pokazuje, w jaki sposób iterator wrócił przez iterpróby uzyskania dostępu do elementów według indeksu. WrappedDictnie dziedziczy po dict, co oznacza, że instancje nie będą miały __iter__metody.
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
Zauważ, że wywołania do __getitem__są delegowane, dict.__getitem__dla których notacja w nawiasach kwadratowych jest po prostu skrótem.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
W punktach 4 i 5: itersprawdza iterator, gdy wywołuje__iter__ :
Kiedy iter(o)zostanie wywołany dla obiektu o, iterupewni się, że zwracana wartość __iter__, jeśli metoda jest obecna, jest iteratorem. Oznacza to, że zwracany obiekt musi implementować __next__(lub nextw Pythonie 2) i __iter__. iternie może wykonać żadnych kontroli poczytalności dla obiektów, które tylko zapewniają __getitem__, ponieważ nie ma możliwości sprawdzenia, czy elementy obiektu są dostępne przez indeks liczb całkowitych.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Zauważ, że konstruowanie iteratora z FailIterIterableinstancji kończy się niepowodzeniem natychmiast, podczas gdy konstruowanie iteratora z FailGetItemIterablepowodzeniem, ale generuje wyjątek przy pierwszym wywołaniu do __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
W punkcie 6: __iter__wygrywa
Ten jest prosty. Jeśli obiekt zaimplementuje __iter__i __getitem__, iterzadzwoni __iter__. Rozważ następującą klasę
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
oraz dane wyjściowe podczas zapętlania instancji:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
W punkcie 7: twoje klasy iterowalne powinny zaimplementować __iter__
Możesz zadać sobie pytanie, dlaczego większość wbudowanych sekwencji, takich jak listimplementacja __iter__metody __getitem__, jest wystarczająca.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
Po tym wszystkim, iteracja przez instancje klasy wyżej, który deleguje do połączenia __getitem__się list.__getitem__(za pomocą notacji nawiasu kwadratowego), będzie działać prawidłowo:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Powody, dla których niestandardowe iteratory powinny zostać zaimplementowane, __iter__są następujące:
- Jeśli zaimplementujesz
__iter__, instancje zostaną uznane za iterowalne i isinstance(o, collections.abc.Iterable)zostaną zwrócone True.
- Jeśli obiekt zwracany przez
__iter__nie jest iteratorem, iternatychmiast zawiedzie i podniesie wartość TypeError.
- Specjalna obsługa
__getitem__istnieje ze względu na kompatybilność wsteczną. Cytując ponownie z płynnego Pythona:
Dlatego każda sekwencja Pythona jest iterowalna: wszystkie implementują __getitem__. W rzeczywistości standardowe sekwencje również się implementują __iter__, a twoja też powinna, ponieważ specjalna obsługa __getitem__istnieje ze względu na kompatybilność wsteczną i może zniknąć w przyszłości (chociaż nie jest przestarzała, kiedy to piszę).
__getitem__wystarcza również, aby obiekt mógł być iterowalny