Я хотел бы, чтобы пролить немного больше света на взаимодействие 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__
метод, а 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__
:
Когда 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__
также достаточно, чтобы сделать объект повторяемым