Я хотів би, щоб пролити трохи більше світла на взаємодію 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__
також достатньо, щоб зробити об’єкт ітерабельним