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 for
pę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) o
ma __iter__
metodę, która zwraca obiekt iteratora. Iteratorem jest dowolny obiekt z metodą __iter__
i __next__
(Python 2 next
:).
b) o
ma __getitem__
metodę.
Sprawdzanie instancji Iterable
lub Sequence
lub kontroli dla atrybutu __iter__
nie jest wystarczające.
Jeśli obiekt o
implementuje tylko __getitem__
, ale nie __iter__
, iter(o)
skonstruuje iterator, który spróbuje pobrać elementy z o
indeksu 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 StopIteration
sam się podniesie .
W najogólniejszym sensie nie ma sposobu, aby sprawdzić, czy powracający iterator iter
jest zdrowy, poza wypróbowaniem go.
Jeśli obiekt zostanie o
zaimplementowany __iter__
, iter
funkcja 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 o
implementuje 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 for
pętlę w Pythonie. Jeśli już wiesz, możesz przejść do następnej sekcji.
Kiedy używasz for item in o
dla 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 next
w Pythonie 2) i __iter__
metodę.
Zgodnie z konwencją __iter__
metoda iteratora powinna zwrócić sam obiekt (tj return self
.). Python następnie wywołuje next
iterator, aż StopIteration
zostanie 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 iter
z instancją BasicIterable
zwróci iterator bez żadnych problemów, ponieważ BasicIterable
implementuje __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Należy jednak zauważyć, że b
nie ma tego __iter__
atrybutu i nie jest uważany za instancję Iterable
lub 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 iter
i zarządzanie potencjałem TypeError
jako 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 x
jest iterowalny, jest wywołanie iter(x)
i obsłużenie TypeError
wyją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 Iterable
ABC nie.
W punkcie 3: Iterowanie nad obiektami, które tylko zapewniają __getitem__
, ale nie zapewniają__iter__
Iteracja po instancji BasicIterable
działa zgodnie z oczekiwaniami: Python konstruuje iterator, który próbuje pobierać elementy według indeksu, zaczynając od zera, aż do IndexError
podniesienia wartości. Metoda obiektu demonstracyjnego __getitem__
zwraca po prostu to, item
co 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ę, StopIteration
gdy nie może zwrócić następnego elementu, a ten, dla IndexError
którego podniesiono, item == 3
jest obsługiwany wewnętrznie. Oto dlaczego zapętlenie BasicIterable
z for
pę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 iter
próby uzyskania dostępu do elementów według indeksu. WrappedDict
nie 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: iter
sprawdza iterator, gdy wywołuje__iter__
:
Kiedy iter(o)
zostanie wywołany dla obiektu o
, iter
upewni się, że zwracana wartość __iter__
, jeśli metoda jest obecna, jest iteratorem. Oznacza to, że zwracany obiekt musi implementować __next__
(lub next
w Pythonie 2) i __iter__
. iter
nie 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 FailIterIterable
instancji kończy się niepowodzeniem natychmiast, podczas gdy konstruowanie iteratora z FailGetItemIterable
powodzeniem, 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__
, iter
zadzwoni __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 list
implementacja __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, iter
natychmiast 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