Я хотел бы, чтобы пролить немного больше света на взаимодействие iter, __iter__а __getitem__и то , что происходит за кулисами. Вооружившись этими знаниями, вы сможете понять, почему лучшее, что вы можете сделать, это
try:
iter(maybe_iterable)
print('iteration will probably work')
except TypeError:
print('not iterable')
Сначала я перечислю факты, а затем сделаю быстрое напоминание о том, что происходит, когда вы используете forцикл в python, после чего следует обсуждение, чтобы проиллюстрировать факты.
факты
Вы можете получить итератор из любого объекта o, вызвав его, iter(o)если выполняется хотя бы одно из следующих условий:
а) oимеет __iter__метод, который возвращает объект итератора. Итератор - это любой объект с методом __iter__и __next__(Python 2 next:).
б) 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__метод, а IterableABC - нет.
По пункту 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__ :
Когда iter(o)вызывается для объекта 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.abc.Iterable)будут возвращаться True.
- Если объект, возвращаемый
__iter__не является итератором, iterнемедленно завершится ошибкой и вызовет a TypeError.
- Специальная обработка
__getitem__существует по причинам обратной совместимости. Цитирую снова из Fluent Python:
Вот почему любая последовательность Python является итеративной: все они реализуются __getitem__. На самом деле, стандартные последовательности также реализуются __iter__, и ваша также должна, потому что специальная обработка __getitem__существует по причинам обратной совместимости и может отсутствовать в будущем (хотя это не рекомендуется, поскольку я пишу это).
__getitem__также достаточно, чтобы сделать объект повторяемым