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