Сообщить об ошибке.

Использование send(), throw() и close() в генераторах Python

Расширенные методы генератора Python

В дополнение к выражению yield и yield from объекты-генераторы могут использовать методы .send(), .throw() и .close().

Разберем работу методов генератора send(), throw() и close() на примере программы, которая будет искать и печатать ЧИСЛОВЫЕ палиндромы (число которое равно самому себе, если его перевернуть). При обнаружении палиндрома программа будет выводить число и осуществлять поиск следующего.

Сначала напишем код самой функции - детектора палиндрома:

def is_palindrome(num):
    # Пропуск чисел без разряда 
    # или с одним разрядом
    if num // 10 == 0:
        return False
    
    # преобразуем число в строку 
    # и переворачиваем ее
    line_num = str(num)[::-1]
    
    # в зависимости от типа данных преобразуем
    # перевернутую строку снова в число
    if isinstance(num, int):
        reversed_num = int(line_num)
    elif isinstance(num, float):
        reversed_num = float(line_num)
    else:
        raise TypeError

    # сравниваем числа
    if num == reversed_num:
        return True
 
    return False

В функцию проверки числа num на палиндром можно не вникать, достаточно знать что функция is_palindrome() возвращает True, если число num является палиндромом или False в противном случае.

Как использовать метод генератора .send()?

Теперь напишем функцию генератор бесконечной последовательности, например, такой:

def infinite_palindromes():
    num = 0
    while True:
        if is_palindrome(num):
            i = (yield num)
            if i is not None:
                num = i
        num += 1

Смотрим в строку, где i = (yield num). Здесь yield используется как выражение, а не как оператор. Это означает, что переменная i принимает значение, которое возвращает yield. Но еще более важно, то что когда выполнение генератора возобновляется, выражение i = (yield num) позволяет получать значение num при помощи метода генератора send() и каждый раз выражение (yield num) будет передавать принятое значение num переменной i.

В следующей строке кода функции infinite_palindromes() переменная i проверяется на None (if i is not None:). Это может произойти, если next() вызывается для объекта генератора и также может произойти, когда выполняется итерация с циклом for. Если i имеет какое то значение, то происходит обновление num этим значением (строка num = i). Но независимо от того, содержит ли i значение, значение num увеличивается на единицу (num += 1) и снова запускается бесконечный цикл while True.

Теперь напишем основной код, который будет отправлять наименьшее число со следующим разрядом десятков обратно в генератор. Например, если палиндром равен 11, то в бесконечный генератор будет отправляться при помощи send() число 100, если 111, то отправиться 1000:

pal_gen = infinite_palindromes()
for i in pal_gen:
    digits = len(str(i))
    pal_gen.send(10 ** digits)

Код выше создает объект генератора и выполняет итерацию по нему. Программа выдает значение только после того, как найден палиндром. Код использует функцию len() для определения количества цифр в этом палиндроме. Затем он посылает 10 ** digits в генератор. Данное поведение возобновляет выполнение генератора и присваивает i значение 10 ** digits. Так как у i есть значение, программа обновляет num и снова проверяет наличие палиндромов.

Как только объект генератора pal_gen найдет и выдаст другой палиндром, то выполнится очередная итерация через цикл for. Это то же самое, что итерация с функцией next(). Генератор в свою очередь выполняет выражение i = (yield num). Однако теперь i-это None, потому что значение num явно не отправляется (при помощи метода send()).

Проще говоря код выше похож на сопрограмму, в которую можно передавать данные. Сопрограммы или функции генераторы полезны для построения конвейеров данных.

Как использовать метод генератора throw()?

Метод генератора throw() позволяет генерировать исключения. В приведенном ниже примере вызываете исключение в строке pal_gen.throw(ValueError("...")). Этот код выдаст исключение ValueError, как только разрядность числа i достигнет 5:

pal_gen = infinite_palindromes()
for i in pal_gen:
    print(i)
    digits = len(str(i))
    if digits == 5:
        pal_gen.throw(ValueError("Большие палиндромы не нужны"))
    pal_gen.send(10 ** (digits))

В коде происходит проверка разрядности i в строке if digits == 5. И если условие выполняется, то метод throw() вызывает исключение ValueError. Этот код выведет что-то похожее:

11
111
1111
10101
Traceback (most recent call last):
  File "pal.py", line 33, in <module>
    pal_gen.throw(ValueError("Большие палиндромы не нужны"))
  File "pal.py", line 22, in infinite_palindromes
    i = (yield num)
ValueError: Большие палиндромы не нужны

Метод генератора throw() полезен в любом месте, где необходимо поймать исключение. В примере выше, метод throw() использовался для прекращения итерации и выхода из программы. Это можно сделать более элегантно при помощи метода генератора close().

Как использовать метод генератора close()?

Как следует из названия метода генератора close() он позволяет остановить генератор. Это может быть особенно удобно при управлении генератором бесконечной последовательности. Давайте обновим приведенный выше код, заменив метод throw() на close(), чтобы остановить итерацию и выйти из программы:

pal_gen = infinite_palindromes()
for i in pal_gen:
    print(i)
    digits = len(str(i))
    if digits == 5:
        pal_gen.close()
    pal_gen.send(10 ** (digits))

Преимущество использования close() состоит в том, что он вызывает исключение StopIteration, которое используется для оповещения об окончании конечного итератора:

11
111
1111
10101
Traceback (most recent call last):
  File "pal.py", line 34, in <module>
    pal_gen.send(10 ** (digits))
StopIteration