Перейти до змісту

Ітератори. генератори

📌 Посилання на тему

📌 Ітератори (Iterators)

Ітератор — це об’єкт, який є:

  • об'єктом-механізмом перебору
  • має метод __iter__(), котрий повертає сам екземпляр ітератора (self)
  • має метод __next__()
  • поступово повертає елементи з колекції по одному, при
    • кожному виклику методу __next__() або функції next();
    • в циклі for
  • пам'ятаю на якому етапі перебору він зараз

  • після завершення викликів викликається виключення StopIteration

Протокол: щоб клас був ітератором, має реалізувати два методи:

  • __iter__() — повертає сам об’єкт ітератора;

  • __next__() — повертає наступний елемент або піднімає StopIteration, коли елементів немає

class Counter(object):
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        'Returns itself as an iterator object'
        return self

    def __next__(self):
        'Returns the next value till current is lower than high'
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

c = Counter(1, 20)
print(next(c)) # вертає 1
print(next(c)) # вертає 2
print(next(c)) # вертає 3

#на 221-й виклик next(c) викине виключення StopIteration

Переваги ітератора

  • Лінива (lazy) обробка даних — елементи обробляються по мірі виклику, економиться пам’ять
  • Можливість створення власної логіки ітерації за допомогою класів.

📌 Ітерабельні об'єкти (iterable objects)

Ітерабельні об’єкти:

  • має дані для перебору
  • не має інформацію (не пам'ятає), на відміну від ітератора, на якому етапі є перебір
  • не має методу __next__()
  • може бути реалізований метод __get_item__
  • має метод, __iter__(), але на відміну від ітераторів, не повертає сам себе, а повертає готовий вже реалізований ітератор в залежності від типу ітерабельного об'єкта (список, кортеж, множина, рядок, словник)
  • кожен раз, коли потрібна ітерація по ітерабельному об'єкту відбувається повернення нового екземпляру ітератора цього об'єкта через метод __iter__()
  • виклик iter(obj) повертає ітератор для нього.
  • Цикл for робить це автоматично

📌 Генератори (Generators)

Генератор — це спеціальний тип ітератора, створений за допомогою функції з ключовим словом yield.

  • Коли така функція викликається, вона не виконується одразу, а повертає об’єкт-генератор
  • якщо виконати функцію next() для отриманого генератора, починається власне виконання коду в середині функції до першого yield, який повертає певне значення й призупиняє функцію, зберігаючи її локальний стан. Наступні виклики next() продовжують виконання від місця паузи
  • замість виклику функції next() для генератора можна викликати в нього метод __next__()
  • для економії пам'яті при обробці даних (напр., двох масивів, читання з файлу і т.д.) можна повертати не самнабір даних, а ітератор (генератор) для отримання його. Він займає значно менше місця.
    • для отримання даних відповідно використати генератор колекцій або цикл for, або функцію-конструктор колекції...
  • бувають
    • генератори-функції
    • генератори-вирази
def my_generator():
    print('High, I am generator')
    yield 1
    yield 2
    yield 3

# на цьому етапі змінні повертається генератор, ніякої логіки в середині функції не виконується
my_g = my_generator() 

# виводиться прінт повідомлення High, I am generator, а також прінт поверненого першого знаячення 1
print(next(my_g)) 

# прінт поверненого значення 2
print(next(my_g)) 

# як альтернатива функції next(), прінт поверненого значення 3
print(my_g.__next__()) 

# викидає виключення StopIteration
print(next(my_g)) 
* Виклик my_generator() повертає генератор в змінну my_g, 
* next(my_g) або my_g.__next__() повертає значення по черзі

Генерація Фібоначчі:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

Повертає нескінченну послідовність ефективно, без витрат пам'яті для всієї послідовності одночасно

Як працює yield

  1. Коли Python бачить у функції yield, він не виконує її як звичайну функцію, а компілює у generator function.

  2. При виклику такої функції код не запускається одразу, замість цього створюється generator object.

  3. Під час першого виклику next(generator) виконання йде до першого yield:

    • Значення після yield віддається викликачеві.
    • Виконання призупиняється — зберігаються:
      • Локальні змінні
      • Позиція в коді
      • Стек викликів
  4. При наступному next() виконання відновлюється з місця, де воно зупинилося.

Особливості методу yield:

  • Призупиняє виконання функції, зберігає стан (локальні змінні, стек, позицію) — подібно до корутин
  • На відміну від return, yield не завершує функцію, а дозволяє продовжити роботу з того самого місця при наступному виклику

📌 Ітератори vs Генератори — порівняння

Характеристика Ітератор Генератор
Реалізація Клас з __iter__() і __next__() Функція з yield
Пам’ять Може зберігати весь стан або дані Лінива генерація, економія пам’яті
Зручність Більше коду, ручне управління станом Менше коду, автоматично зберігається стан
Взаємовідношення Не обов’язково генератор Кожен генератор — це ітератор
Статус iterator generator (підклас Iterator) ([DataFlair][1])
  • Генератори — це зручні й ефективні реалізації ітераторів, створені автоматично Python-ом через функції з yield
  • Ітератори створюються руками для більш складної логіки й контролю.

📌 Генераторні вирази (Generator Expressions)

  • Схожі на генератори списків (list comprehensions), але в круглих дужках;
  • Вираз генератора не створює відразу готовий набір значень, а повертає об’єкт генератора, який:
    • не зберігає всі результати в пам’яті
    • видає значення поступово, коли ти їх запитуєш
  • Отримати значення генератора можна:
    • Через цикл for
    • Через функцію next() або метод генератора .__next__()
    • Одноразово перетворивши на іншу структуру
  • ⚠ Важливий момент - після того, як генератор "видав" всі значення, він вичерпується — повторно пройтися ним не вийде без створення нового.

Отримати значення Через цикл for

g = (x**2 for x in range(5))
print(g)  
# <generator object <genexpr> at 0x...>

for val in g:
    print(val)

Отримати значення Через функцію next() або метод генератора .__next__()

squares = (n * n for n in range(4))

print(squares) # <generator object <genexpr> at 0x000002A8BAC8BAC0>
print(next(squares)) # 0
print(squares.__next__()) # 1
print(next(squares)) # 4
print(squares.__next__()) # 9
print(next(squares)) # exception StopIteration

Отримати значення Через функцію next() або метод генератора .__next__()

g = (x**2 for x in range(5))

print(list(g))  # [0, 1, 4, 9, 16]

print(list(g))  # [] - пустий список, бо генератор вже не повертає жодних значень, він вже відпрацьований