Ітератори
Щоб об'єкт став ітерабельним, з нього треба побудувати ще один спеціальний об'єкт, який називається ітератор. Для певного ітерабельного об'єкта за допомогою ітератора ми будемо отримувати значення з цього ітерабельного об'єкта. Ітератор як би створює потік даних, які містяться у ітерабельному об'єкті.
Ітератор є "одноразовим": якщо ми знову захочемо "пробігтись" по ітерабельному об'єкту, то нам треба буде отримати новий ітератор для цього ітерабельлного об'єкта.
Отримати ітератор для певного об'єкта можна за допомогою функції iter()
.
Ця функція робить наступне:
- Перевіряє, чи містить переданий ії об'єкт магічний метод
__iter__()
. Якщо так, то вона викликає цей магічний метод, а він у свою чергу повинен повернути об`єкт-ітератор. - Якщо ж такого магічного метода немає,
тоді функція
iter()
очікує що переданий їй об'єкт має магічний метод__getitem__()
. У такому випадку Python побудує і створить ітератор самостійно.
Отже, щоб об'єкт був ітерабельним,
він повинен коректно реалізовувати один з двох або ж обидва магічні методи: __iter__()
та __getitem__()
.
При таких умовах Python може побудувати ітератор для даного об'єкта.
І, відповідно, такий об'єкт ми можемо використовувати у циклі for
,
передавати функціям які очікують послідовності, тощо.
Давайте спробуємо отримати ітератори для деяких сутностей Python:
>>> i = iter('hello')
>>> i
<str_iterator object at 0x0000020BDD63F198>
>>> i = iter([1,3,5,7])
>>> i
<list_iterator object at 0x0000020BDD63F1D0>
>>> i = iter({})
>>> i
<dict_keyiterator object at 0x0000020BDD32A3B8>
>>> i = iter({}.values())
>>> i
<dict_valueiterator object at 0x0000020BDD306638>
>>> i = iter({}.items())
>>> i
<dict_itemiterator object at 0x0000020BDD32A3B8>
>>>
Отримання значень ітерабельних об'єктів
Щоб отримати від ітератора чергове значення ітерабельного об'єкта,
починаючи з першого,
використовують функцію next()
.
Ця функція у свою чергу викликає магічний метод ітератора __next__()
.
При кожному виклику метод __next__()
повертає чергове значення з ітерабельного об'єкта,
аж доки ці значення не закінчаться.
Коли ж усі значення вичерпано,
при черговому і кожному наступному виклику __next__()
цей метод має викинути один з винятків —
StopIteration
або IndexError
:
>>> i = iter('bye')
>>> next(i)
'b'
>>> next(i)
'y'
>>> next(i)
'e'
>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
Крім того кожен ітератор має реалізовувати магічний метод __iter__()
(так само, як і сам ітерабельний об'єкт).
Цей метод має повертати сам ітератор, зазвичай він виглядає так:
def __iter__(self):
return self
Можна зробити висновок, що ітератор і сам є ітерабельним об'єктом і може бути використаним всюди, де очікуються ітерабельні об'єкти.
Отже циклом for
ми можемо "пробігтись" як по самому ітерабельному об'єкту,
так і по його ітератору.
Але треба пам'ятати,
що по самому ітерабельному об'єкту ми можемо ітеруватись багато раз,
а по його ітератору лише один раз.
Як працює for
По усім вищенаведеним правилам працює і вже добре відомий нам цикл for
:
- створює для ітерабельного об'єкта ітератор
- потім за допомогою ітератора отримує чергове значення для ітерабельного об'єкта
до тих пір,
доки не виникне виняток
StopIteration
Ми навіть можемо самостійно реалізувати такий цикл:
>>> def my_for(iterable, callback_func):
... iterator = iter(iterable)
... while True:
... try:
... value = next(iterator)
... callback_func(value)
... except StopIteration:
... break
...
>>> def loop_body(value):
... print(value)
...
>>> my_for('bye', loop_body)
b
y
e
>>>
Просто, і ніякої магії!
Така сама логіка реалізована для цикла for
.