Skip to content

Iterator and Generator

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

Генератор vs Ітератор

Різниця між генераторами та ітераторами в Python полягає в тому, що ітератори використовуються для перебору групи елементів (наприклад, у списку), тоді як генератори є способом реалізації ітераторів, і використовуються для генерації значень. Генератори використовують ключове слово yield для повернення значення з функції, але, за винятком цього, вони ведуть себе як звичайні функції.

Links

Ітерабельний об'єкт

Summary

Ітерабельний об'єкт (iterable) - це об'єкт, який може повертати значення по одному за раз. Приклади: всі контейнери і послідовності (списки, рядки і т.д.), файли, а також екземпляри будь-яких класів, в яких визначений метод __iter__().

Тобто, ітерабельний об'єкт - це будь-який об'єкт, від якого вбудована функція iter() може отримати ітератор, тобто який реалізує метод __iter__.

Ітерабельні об'єкти можна використовувати у циклі for, а також в багатьох інших випадках, коли очікується послідовність (функції sum(), zip(), map() і т.д.).

Розглянемо ітерабельний об'єкт (Iterable). У стандартній бібліотеці він оголошений як абстрактний клас collections.abc.Iterable:

class Iterable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __iter__(self):
        while False:
            yield None

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterable:
            return _check_methods(C, "__iter__")
        return NotImplemented

Він має абстрактний метод __iter__, який повинен повернути об'єкт ітератора. І метод __subclasshook__, який перевіряє наявність у класу методу __iter__.

class SomeIterable1(collections.abc.Iterable):
    def __iter__(self):
        pass

class SomeIterable2:
    def __iter__(self):
        pass

print(isinstance(SomeIterable1(), collections.abc.Iterable))  # True

print(isinstance(SomeIterable2(), collections.abc.Iterable))  # True

Але є один момент - це функція iter(). Наприклад, цю функцію використовує цикл for для отримання ітератора. Функція iter() спочатку намагається отримати ітератор з об'єкта, викликаючи його метод __iter__. Якщо метод не реалізовано, то вона перевіряє наявність метода __getitem__, і якщо він реалізований, то на його основі створюється ітератор. __getitem__ повинен приймати індекс з нуля. Якщо ж жоден з цих методів не реалізовано, то виникає виключення TypeError.

from string import ascii_letters

class SomeIterable3:
    def __getitem__(self, key):
        return ascii_letters[key]

for item in SomeIterable3():
    print(item)

Наприклад, по list можна ітеруватися, але сам list ніяк не стежить, де ми зупинилися в проході по ньому. А стежить об'єкт на ім'я ListIterator, який повертається методом iter() і використовується, наприклад, циклом for.

Ітератор

Summary

Ітератор - об'єкт, який повертає свої елементи по одному за раз, підтримує ітерацію по послідовності. З точки зору Python він повинен мати метод __iter__(), який повертає сам об'єкт ітератора, і метод __next__(), який повертає наступний елемент послідовності або викидає виняток StopIteration, якщо більше немає елементів.

Ітератори використовуються в циклі for для ітерації по колекції. Кожен об'єкт, який підтримує ітератор, є ітерабельним, але не кожен ітерабельний об'єкт є ітератором. Наприклад рядки та словники - оскільки вони не мають методу __next__, натомість ітерабельність забезпечується наявністю методу __getitem__, який дозволяє отримувати доступ до елементів за їхніми індексами чи ключами.

Ітерабельність - це властивість об'єкта підтримувати ітерацію. Всі послідовності в Python є ітерабельними, оскільки вони підтримують ітерацію через свої елементи.

Ітератори представлені абстрактним класом collections.abc.Iterator:

class Iterator(Iterable):

    __slots__ = ()

    @abstractmethod
    def __next__(self):
        """Return the next item from the iterator. When exhausted, raise StopIteration"""
        raise StopIteration

    def __iter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')
        return NotImplemented
  • __next__ повертає наступний доступний елемент і викликає виняток StopIteration, коли елементів не залишилося.
  • __iter__ повертає self. Це дозволяє використовувати ітератор там, де очікується ітерабельний об'єкт, наприклад, в циклі for.
  • __subclasshook__ перевіряє наявність у класу методів __iter__ і __next__

По ітератору можна пройтись тільки один раз.

my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)

print(next(my_iterator))  # 1
print(next(my_iterator))  # 2
print(next(my_iterator))  # 3

for item in my_iterator:
    print(item)  # 4, 5

try:
    print(next(my_iterator))  # raises StopIteration
except StopIteration:
    print("Iteration is done")

Генератор

В залежності від контексту, може означати або функцію-генератор, або ітератор генератора (зазвичай останнє). Методи __iter__ і __next__ для генераторів створюються автоматично.

Генератор - це лінивий ітератор. Генератор не зберігає в пам'яті всі елементи, а лише внутрішній стан для обчислення наступного елемента. На кожному кроці можна обчислити лише наступний елемент, але не попередній. Пройти генератор в циклі можна лише один раз.

Переваги використання генераторів - Економія пам'яті- генератори працюють ліниво, генеруючи значення за вимогою. Можуть використовуватися для завантаження даних у міру необхідності, що корисно при роботі з великими файлами або мережевими ресурсами, що дозволяє економити пам'ять - Ефективна обробка даних - генератори полегшують обробку великих наборів даних, тому що вони можуть обробляти дані поелементно, що може бути більш ефективним, ніж завантаження та обробка всіх даних одразу.

З точки зору реалізації, генератор в Python - це мовна конструкція, яку можна реалізувати двома способами: як функцію з ключовим словом yield або як генераторний вираз. При виклику функції або обчисленні виразу отримуємо об'єкт-генератор типу types.GeneratorType. Класичний приклад - генератор, який породжує послідовність чисел Фібоначчі, яка, будучи нескінченною, не могла б поміститися в будь-яку колекцію. Іноді термін використовується для самої генераторної функції, а не тільки для об'єкта, який повертається.

Оскільки в об'єкті-генераторі визначені методи __next__ і __iter__, тобто реалізований протокол ітератора, то в Python будь-який генератор є ітератором.

Коли виконання функції-генератора завершується (за допомогою ключового слова return або досягненням кінця функції), виникає виняток StopIteration.

Що таке генераторна функція

Генераторна функція - це спеціальний тип функції, в тілі якої зустрічається ключове слово yield. При виклику такої функції повертається об'єкт-генератор (generator object). Генератори використовуються для створення ітерабельних об'єктів, що генерують значення "на льоту", зазвичай без зберігання їх у пам'яті. Якщо звичайна функція має одну точку входу та одне значення, яку повертається, то генераторна може мати кілька точок входу і виходу.

def generate_numbers(n):
    i = 0
    while i < n:
        yield i
        i += 1

generator = generate_numbers(5)  # <generator object iterate at 0x...>
for number in generator:
    print(number)

Важливо пам'ятати, що генератор можна пройти тільки один раз.

Що робить yield

yield заморожує стан функції-генератора і повертає поточне значення. Після наступного виклику __next__() функція-генератор продовжує своє виконання з того місця, де вона була призупинена.

Що робить yield from ?

В Python ключове слово yield from спрощує делегування генераторів. Воно використовується для передачі управління іншому генератору або ітерованому об'єкту з основного генератора. Це дозволяє зменшити обсяг коду та полегшити роботу з вкладеними генераторами.

yield from автоматично ітерується по вказаному генератору чи ітерованому об'єкту і повертає всі його значення. При цьому усі send(), throw(), і close() команди передаються делегованому генератору. Якщо делегований генератор завершується з допомогою return, значення, яке повертається, можна отримати в основному генераторі через StopIteration.

def subgen():
    yield 1
    yield 2
    return "Done"

def main_gen():
    result = yield from subgen()
    print(result)
    yield 3

for val in main_gen():
    print(val)  # 1 2 Done 3

Для чого використовуються .close(), .throw() і .send() ?

Методи .close(), .throw() і .send() використовуються для управління генераторами в Python, розширюючи їх стандартну функціональність і дозволяючи більш складну взаємодію з ними.

  • .send(value):
    Цей метод дозволяє передати значення всередину генератора. Він відновлює виконання генератора і вставляє передане значення в місце, де знаходиться вираз yield. Це корисно, якщо генератор повинен реагувати на вхідні дані під час виконання.
def counter():
    total = 0
    while True:
        value = yield total  # Yield current total and receive new value
        if value is not None:
            total += value

gen = counter()
print(next(gen))  # Start generator: output 0
print(gen.send(10))  # Add 10: output 10
print(gen.send(5))   # Add 5: output 15
  • .throw(exc_type, value=None, traceback=None):
    Використовується для ін'єкції винятку в генератор у місце, де він знаходиться. Генератор може обробити цей виняток або дозволити його піднятися вище. Це корисно для тестування обробки помилок у генераторі.
def sample_gen():
    try:
        yield "Start"
    except ValueError:
        yield "Handled ValueError"
    yield "End"

gen = sample_gen()
print(next(gen))  # Start generator
print(gen.throw(ValueError))  # Inject exception: output "Handled ValueError"
print(next(gen))  # Output "End"
  • .close():
    Завершує виконання генератора, піднімаючи виняток GeneratorExit. Якщо генератор обробляє цей виняток, він може виконати код очищення перед завершенням. Після виклику .close() генератор не можна більше відновити.
def example_gen():
    try:
        yield "Running"
    finally:
        print("Generator is closing.")

gen = example_gen()
print(next(gen))  # Start generator
gen.close()  # Close generator: triggers `finally` block

В чому відмінність [x for x in y] від (x for x in y)

Перший вираз повертає список (списковий вираз), другий - генератор.

Як отримати список з генератора

Передати його у конструктор списку: list(x for x in some_seq). Важливо, що після цього по генератору не можна буде ітеруватись.

Що таке підгенератор

У Python 3 існують так звані підгенератори (subgenerators). Якщо у генераторній функції зустрічається пара ключових слів yield from, за якими слідує об'єкт-генератор, то цей генератор делегує доступ до підгенератора, доки він не завершиться (не закінчаться його значення), після чого продовжує своє виконання.

Насправді, yield є виразом. Він може приймати значення, які надсилаються у генератор. Якщо значення не надсилаються у генератор, результатом цього виразу є None.

yield from також є виразом. Результатом його є значення, яке підгенератор повертає у виключенні StopIteration (для цього значення повертається за допомогою ключового слова return).

Які методи є у генераторів

  • __next__() - починає або продовжує виконання функції-генератора. Результатом поточного виразу yield буде None. Виконання потім продовжується до наступного виразу yield, який передає значення до місця, де був викликаний __next__. Якщо генератор завершується без повернення значення за допомогою yield, виникає виключення StopIteration. Зазвичай метод викликається неявно, наприклад, циклом for або вбудованою функцією next().
  • send(value) - продовжує виконання і надсилає значення у функцію-генератор. Аргумент value стає значенням поточного виразу yield. Метод send() повертає наступне значення, повернене генератором, або викликає виключення StopIteration, якщо генератор завершується без повернення значення. Якщо send() використовується для запуску генератора, єдиним допустимим значенням є None, оскільки ще не було виконано жодного виразу yield, якому можна присвоїти це значення.
  • throw(type[, value[, traceback]]) - викликає виняток типу type у місці, де було призупинено генератор, і повертає наступне значення генератора (або викликає StopIteration). Якщо генератор не обробляє даний виняток (або викликає інший виняток), то він виникає у місці виклику.
  • close() - викликає виняток GeneratorExit у місці, де було призупинено генератор. Якщо генератор викликає StopIteration (через нормальне завершення або через те, що він вже закритий) або GeneratorExit (через відсутність обробки цього винятку), close просто повертається до місця виклику. Якщо ж генератор повертає наступне значення, виникає виняток RuntimeError. Метод close() нічого не робить, якщо генератор вже завершений.

Чи можна отримати елемент генератора за індексом

Ні, виникне помилка. Генератор не підтримує метод __getitem__.

Що таке співпрограма

Співпрограма (coroutine) - це спеціальна функція, яка може призупиняти своє виконання та передавати управління іншим корутинам, а потім продовжувати з місця, де зупинилися.

Корутини можеть мати кілька точок входу та виходу, на відміну від звичайних підпрограм, які мають одну точку входу та одну точку виходу. Також вони можуть зупиняти своє виконання будь-якої миті за допомогою спеціального оператора (наприклад, yield в Python або await в Kotlin), зберігаючи свій стан (локальні змінні та стек викликів).

Корутини працюють кооперативно - віддають управління одне одному, не конкуруючи за ресурси.

Для реалізації співпрограм використовуються розширені можливості генераторів у Python (вирази yield і yield from, надсилання значень у генератори).

Співпрограми корисні для реалізації асинхронних неблокуючих операцій та кооперативної багатозадачності у одному потоці без використання зворотних викликів (callback-функцій) та написання асинхронного коду у синхронному стилі.

Python 3.5 включає підтримку співпрограм на рівні мови. Для цього використовуються ключові слова async і await.