Я хотел бы пролить немного больше света на взаимодействие iter
, __iter__
и __getitem__
и то, что происходит за кулисами. Вооружившись этими знаниями, вы сможете понять, почему лучшее, что вы можете сделать, это
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Сначала я перечислю факты, а затем кратко напомню, что происходит, когда вы используете цикл for
в python, после чего следует обсуждение, чтобы проиллюстрировать факты.
Факты
Вы можете получить итератор из любого объекта o
, вызвав iter(o)
, если выполняется хотя бы одно из следующих условий:
a) o
имеет метод __iter__
который возвращает объект итератора. Итератор - это любой объект с __iter__
и __next__
(Python 2: next
) методом.
b) o
имеет метод __getitem__
.
Проверка экземпляра Iterable
или Sequence
или проверка наличия
атрибута __iter__
недостаточно.
Если объект o
реализует только __getitem__
, но не __iter__
, iter(o)
создаст
итератор, который пытается извлечь элементы из o
по целочисленному индексу, начиная с индекса 0. Итератор будет перехватывать любые IndexError
(но не другие ошибки), которые возникают, а затем сами вызывают StopIteration
.
В самом общем смысле, нет способа проверить, является ли итератор, возвращаемый iter
нормальным, кроме как попробовать его.
Если объект o
реализует __iter__
, функция iter
обеспечит
что объект, возвращаемый __iter__
, является итератором. Там нет проверки вменяемости
если объект реализует только __getitem__
.
__iter__
побед. Если объект o
реализует как __iter__
, так и __getitem__
, iter(o)
вызовет __iter__
.
Если вы хотите сделать свои собственные объекты повторяемыми, всегда используйте метод __iter__
.
for
петли
Чтобы следовать, вам нужно понять, что происходит, когда вы используете цикл for
в Python. Не стесняйтесь переходить к следующему разделу, если вы уже знаете.
Когда вы используете for item in o
для некоторого итерируемого объекта o
, Python вызывает iter(o)
и ожидает объект итератора в качестве возвращаемого значения. Итератор - это любой объект, который реализует метод __next__
(или next
в Python 2) и метод __iter__
.
По соглашению, метод __iter__
итератора должен возвращать сам объект (т.е. return self
). Затем Python вызывает итератор next
до тех пор, пока не будет поднято StopIteration
. Все это происходит неявно, но следующая демонстрация делает это видимым:
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
Итерация по 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
Обсуждение и иллюстрации
По пунктам 1 и 2: получение итератора и ненадежные проверки
Рассмотрим следующий класс:
class BasicIterable(object):
def __getitem__(self, item):
if item == 3:
raise IndexError
return item
Вызов iter
с экземпляром BasicIterable
вернет итератор без проблем, поскольку BasicIterable
реализует __getitem__
.
>>> b = BasicIterable()
>>> iter(b)
<iterator object at 0x7f1ab216e320>
Однако важно отметить, что b
не имеет атрибута __iter__
и не считается экземпляром Iterable
или Sequence
:
>>> from collections import Iterable, Sequence
>>> hasattr(b, '__iter__')
False
>>> isinstance(b, Iterable)
False
>>> isinstance(b, Sequence)
False
Вот почему Fluent Python от Luciano Ramalho рекомендует вызывать iter
и обрабатывать потенциал TypeError
как наиболее точный способ проверить, является ли объект итеративным. Цитирую прямо из книги:
Начиная с Python 3.4, наиболее точный способ проверить, является ли объект x
итеративным, - это вызвать iter(x)
и обработать исключение TypeError
, если это не так. Это точнее, чем использование isinstance(x, abc.Iterable)
, поскольку iter(x)
также учитывает устаревший метод __getitem__
, а Iterable
ABC - нет.
В пункте 3: перебор объектов, которые предоставляют только __getitem__
, но не __iter__
Итерации по экземпляру BasicIterable
работают как положено: Python
создает итератор, который пытается извлечь элементы по индексу, начиная с нуля, до тех пор, пока не будет поднято IndexError
. Метод __getitem__
демонстрационного объекта просто возвращает item
, который был передан в качестве аргумента __getitem__(self, item)
итератором, возвращаемым 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
Обратите внимание, что итератор вызывает StopIteration
, когда он не может вернуть следующий элемент, и что IndexError
, который вызывается для item == 3
, обрабатывается внутренне. Вот почему цикл по BasicIterable
с циклом for
работает, как и ожидалось:
>>> for x in b:
... print(x)
...
0
1
2
Вот еще один пример, чтобы показать, как итератор, возвращаемый iter
, пытается получить доступ к элементам по индексу. WrappedDict
не наследуется от dict
, что означает, что экземпляры не будут иметь метод __iter__
.
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
Обратите внимание, что вызовы __getitem__
делегируются dict.__getitem__
, для которых запись в квадратных скобках является просто сокращением.
>>> w = WrappedDict({-1: 'not printed',
... 0: 'hi', 1: 'StackOverflow', 2: '!',
... 4: 'not printed',
... 'x': 'not printed'})
>>> for x in w:
... print(x)
...
hi
StackOverflow
!
В пунктах 4 и 5: iter
проверяет наличие итератора при вызове __iter__
:
Когда для объекта o
вызывается iter(o)
, iter
будет гарантировать, что возвращаемое значение __iter__
, если метод присутствует, является итератором. Это означает, что возвращаемый объект
должен реализовывать __next__
(или next
в Python 2) и __iter__
. iter
не может выполнять проверки работоспособности объектов, которые только
укажите __getitem__
, поскольку он не может проверить, доступны ли элементы объекта по целочисленному индексу.
class FailIterIterable(object):
def __iter__(self):
return object() # not an iterator
class FailGetitemIterable(object):
def __getitem__(self, item):
raise Exception
Обратите внимание, что создание итератора из FailIterIterable
экземпляров завершается неудачно, а создание итератора из FailGetItemIterable
завершается успешно, но при первом вызове __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
В точке 6: __iter__
выигрывает
Это просто. Если объект реализует __iter__
и __getitem__
, iter
вызовет __iter__
. Рассмотрим следующий класс
class IterWinsDemo(object):
def __iter__(self):
return iter(['__iter__', 'wins'])
def __getitem__(self, item):
return ['__getitem__', 'wins'][item]
и вывод при зацикливании на экземпляр:
>>> iwd = IterWinsDemo()
>>> for x in iwd:
... print(x)
...
__iter__
wins
По пункту 7: ваши итерируемые классы должны реализовывать __iter__
Вы можете спросить себя, почему большинство встроенных последовательностей, таких как list
, реализуют метод __iter__
, когда достаточно __getitem__
.
class WrappedList(object): # note: no inheritance from list!
def __init__(self, lst):
self._list = lst
def __getitem__(self, item):
return self._list[item]
В конце концов, перебор экземпляров класса выше, который делегирует вызовы от __getitem__
до list.__getitem__
(используя квадратную скобку), будет работать нормально:
>>> wl = WrappedList(['A', 'B', 'C'])
>>> for x in wl:
... print(x)
...
A
B
C
Причины, по которым ваши пользовательские итерации должны реализовывать __iter__
, заключаются в следующем:
- Если вы реализуете
__iter__
, экземпляры будут считаться итеративными, а isinstance(o, collections.Iterable)
вернет True
.
- Если объект, возвращаемый
__iter__
, не является итератором, iter
немедленно завершится ошибкой и вызовет TypeError
.
- Специальная обработка
__getitem__
существует в целях обратной совместимости. Цитирую снова из Fluent Python:
Именно поэтому любая последовательность Python является итеративной: все они реализуют __getitem__
. По факту,
стандартные последовательности также реализуют __iter__
, и ваш тоже должен, потому что
специальная обработка __getitem__
существует по причинам обратной совместимости и может быть
ушел в будущее (хотя это не устарело, когда я пишу это).