В статье Python: генераторные функции я рассказал, как создавать объекты-генераторы и управлять их выполнением через yield.

Но генератор — это больше, чем просто итератор. Методы send, throw и close позволяют не только получать из него данные, но и вмешиваться в сам процесс выполнения: отправлять значения прямо «в середину», вбрасывать внутрь исключения или завершать работу особым образом.

А ещё…

  • в Python 3.13 метод close получил неожиданное улучшение

  • есть случай, когда поведение CPython расходится с The Python Language Reference

  • и в PEP 342 спрятана пара интересных деталей

Все эти моменты мы разберём на работающих примерах — от очевидных до таких, что способны удивить даже опытного питониста.

Все примеры, если не указано иное, проверены на Python 3.10.

Генераторное выражение

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

Вот простой пример генераторного выражения.

>>> gen = (i for i in range(10))

Проверим, что это генератор и переберем его элементы при помощи конструктора list.

>>> gen
<generator object <genexpr> at 0x7a0129527bc0>
>>> type(gen)
<class 'generator'>
>>> from inspect import isgenerator
>>> isgenerator(gen)
True
>>> list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Все описанное далее относится как к генераторным функциям, так и к генераторным выражениям, — так как оба этих способа создают объекты генераторов.

Метод send

Из примеров статьи Python: генераторные функции может сложиться ложное впечатление, что функция-генератор приостанавливается после обработки yield инструкции, и возобновляет работу с выполнения инструкции, следующей непосредственно за yield.

На самом деле yield — это не инструкция (statement), а выражение (yield expression)1.

В том что yield — это именно выражение, мы можем убедиться, обратившись к его значению.

>>> def generator_function():  
...     for i in range(3):  
...         print("yield value:", (yield i))  
...  
>>> generator_instance = generator_function()  
>>> next(generator_instance)  
0  
>>> next(generator_instance)  
yield value: None  
1  

Когда next вызывается в первый раз, функция-генератор доходит до yield выражения и приступает к его вычислению. В ходе вычисления выдается значение i как первый элемент итератора (0) — вместе с передачей управления. На этом вычисление yield выражения не заканчивается. В противном случае мы получили бы значение, передали бы его в функцию print и вывели на печать.

Точка передачи управления
Точка передачи управления

И только на втором вызове next — когда управление возвращается в генераторную функцию — она завершает вычисление yield выражения и передает его значение (None) в функцию print.

После этого генераторная функция заходит на вторую итерацию цикла for и, дойдя вновь до выражения yield, отдает как второй элемент генератора значение выражения справа от yield (1).

В следующем примере мы увидим, что результатом вычисления yield выражения может быть не только None, но и любое значение — то, которое мы передадим в метод генератора send.

>>> def squarer():
...     number = yield
...     while number is not None:
...         print(f"squarer got {number}")
...         number = yield number**2
... 
>>> gen = squarer()
>>> next(gen)
>>> print(f"2\u00B2", "=", gen.send(2))
squarer got 2
2² = 4
>>> print(f"5\u00B2", "=", gen.send(5))
squarer got 5
5² = 25

Сразу после создания генератора мы вызываем next(gen) — чтобы функция-генератор дошла до первого yield выражения. Оно выдает None — поэтому значение next(gen) не выводится в REPL.

Далее мы вызываем метод генератора send. Передаваемый ему параметр (2) становится значением yield выражения и сохраняется функцией squarer в переменную number. После этого функция squarer заходит в цикл while, выдает number**2 (4) и передает управление в ожидании следующего вызова send.

Как мы видим, метод send — также как и функция next — приводит к одному шагу итерации объекта-генератора. Значение функции send — также как и функции next — это элемент генератора, выдаваемый yield выражением. При этом параметр, переданный методу send, становится значением этого yield выражения.

И так, у нас получился бесконечный итератор.

Мы можем исчерпать его, лишь передав в генератор значение None — оно приведет к выходу из цикла while.

В предыдущем примере мы видели, что None значение передается в генератор при вызове функции next.

То есть, для объекта-генератора эти выражения являются равносильными:

gen.send(None)
next(gen)

Действительно, оба они передают значение None в yield выражение, что возобновляет работу функции-генератора. Проверим это.

определенная ранее генераторная функция
>>> def squarer():
...     number = yield
...     while number is not None:
...         print(f"squarer got {number}")
...         number = yield number**2
... 
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

После предыдущих манипуляций генераторная функция находилась внутри цикла while, а здесь функция next передает в генератор None значение. В результате генераторная функция выходит из цикла и завершается, что и приводит к StopIteration исключению.

Говоря о функции next, стоит отметить, что она принимает второй опциональный агрумент — default. С ним функция next перехватит StopIteration исключение и вернет для такого случая переданное в default значение.

>>> next(gen)  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> next(gen, "maybe")
'maybe'

Однако метод send не предоставляет такую возможность — и это логично. StopIteration исключение указывает на то, что генераторная функция завершила свое выполнение. Значит, не может быть yield выражения, ожидающего значения от метода send. То есть, значение будет проигнорировано. А попытка его туда все-таки передать может быть ошибкой в логике программы.

По тем же причинам попытка передать во вновь созданный генератор любое значение, кроме None, приводит к исключению TypeError. Действительно, если генераторная функция ещё не запущена и ни одно выражение yield не вычисляется, то передавать значение попросту некуда.

Давайте посмотрим, какие значения можно передавать в генератор на первой итерации.

>>> gen_1 = squarer()
>>> gen_2 = squarer()
>>> gen_3 = squarer()
>>> gen_1.send(None)  # ок
>>> next(gen_2)  # то же что gen_2.send(None)
>>> gen_3.send(0)  # а вот так - нельзя
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

Теперь о генераторных выражениях. Они также предоставляют метод send. Но генератор игнорирует передаваемые в метод значения (кроме первого, поведение для которого я только что описал).

>>> gen = (i for i in range(100))
>>> gen.send("something")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> gen.send(None)
0
>>> gen.send("eniki")
1
>>> gen.send("beniki")
2

Метод throw

Помимо передачи в генератор значения посредством send, в нем также можно возбудить исключение при помощи метода throw.

Если исключение не перехвачено, генератор завершается и распространяет исключение на вызывающий контекст. Последующее итерирование генератора выдаст StopIteration.

Проверим на примере генераторного выражения, что это верно как для вновь созданного, так и для запущенного и для уже завершившегося генератора.

>>> gen = (i for i in range(10))
>>> gen.throw(Exception("unstarted generator"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
Exception: unstarted generator
>>> next(gen, "EMPTY")
'EMPTY'
>>> gen = (i for i in range(10))
>>> next(gen)
0
>>> gen.throw(Exception("ran generator"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
Exception: ran generator
>>> next(gen, "EMPTY")
'EMPTY'
>>> gen = (i for i in range(10))
>>> list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> next(gen, "EMPTY")
'EMPTY'
>>> gen.throw(Exception("closed generator"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: closed generator

Здесь мы проверяли то что генератор исчерпан, обращаясь к default значению метода next. В последнем примере мы исчерпали генератор до вызова throw, передав его в конструктор list.

Генераторная функция может перехватить исключение, вызванное методом throw, как если бы его инициировал текущий yield. После перехвата исключения функция сможет продолжить свою работу и, дойдя до следующего yield выражения, выдать следующий элемент генератора. Он и станет значением функции throw, возбудившей исключение.

>>> def gen_f():
...     for i in range(3):
...         try:
...             yield i
...         except Exception:
...             pass
... 
>>> gen = gen_f()
>>> next(gen)
0
>>> gen.throw(Exception("Boo!"))
1
>>> next(gen)
2
>>> next(gen, "EMPTY")
'EMPTY'

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

>>> def gen_f():
...     try:
...         yield "first"
...         yield "second"
...     except Exception:
...         pass
... 
>>> gen = gen_f()
>>> gen.throw(Exception("to unstarted"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in gen_f
Exception: to unstarted
>>> next(gen, "EMPTY")
'EMPTY'
>>> gen = gen_f()
>>> list(gen)
['first', 'second']
>>> next(gen, "EMPTY")
'EMPTY'
>>> gen.throw(Exception("to closed"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: to closed

Напишем генераторную функцию, которая получает строки через send. При возбуждении исключения SummarizeAndReset она выдает конкатенацию переданных строк как элемент генератора и одновременно сбрасывает состояние, очищая список ранее переданных строк. Начнем в IDLE и продолжим в REPL.

class SummarizeAndReset(Exception):
    pass

def string_summarizer():
    strings = []
    item = None    
    while True:        
        try:
            string = yield item
            item = None
            if isinstance(string, str):
                strings.append(string)            
        except SummarizeAndReset:
            item = ",".join(strings)
            strings = []
>>> gen = string_summarizer()
>>> next(gen)  # Запускаем генератор
>>> gen.send("cat")
>>> gen.send("dog")
>>> gen.send("rabbit")
>>> gen.throw(SummarizeAndReset())
'cat,dog,rabbit'
>>> gen.send("bear")
>>> gen.send("wolf")
>>> gen.throw(SummarizeAndReset())
'bear,wolf'

Метод close

В прошлом примере мы так и не завершили работу генераторной функции. Да, она все еще висит в памяти, ожидая следующей итерации. И, если мы не выйдем из нее до конца этой статьи, потом такая возможность уже не представится.

Давайте сделаем это прямо сейчас.

>>> gen.close()

Проверим состояние генератора

>>> next(gen)
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    next(gen)
StopIteration

Ура! Генератор исчерпан.

Метод close завершает генераторную функцию, прекращая генерацию новых значений.

Убедимся на генераторном выражении что это также верно для вновь созданного генератора.

>>> gen = (i for i in range(10))
>>> gen.close()
>>> next(gen, "EMPTY")
'EMPTY'

Под капотом метод close возбуждает в генераторе исключение GeneratorExit . При этом оно не распространяется в вызывающий контекст, даже не будучи обработанным. В этом случае, по заветам классика, его ловит сам генератор: "Я тебя породил — я тебя и перехвачу".

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

Посмотрим на примере как это работает.

>>> def gen_f():
...     for i in range(100):
...         try:
...             yield i
...         except GeneratorExit:
...             print("exit")
...             return
... 
>>> gen = gen_f()
>>> next(gen), next(gen), next(gen)
(0, 1, 2)
>>> gen.close()
exit

Обратите внимание на инструкцию return в блоке обработки исключения. Без нее функция продолжала бы выдавать новые элементы генератора. За такое Python жестко карает RuntimeError’ом. Не для того генератор закрывают — чтобы он дальше значения выдавал!

>>> def gen_f():
...     for i in range(100):
...         try:
...             yield i
...         except GeneratorExit:
...             print("exit")
... 
>>> gen = gen_f()
>>> next(gen), next(gen), next(gen)
(0, 1, 2)
>>> gen.close()
exit
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: generator ignored GeneratorExit

Работая с исключением GeneratorExit важно знать, что оно наследуется напрямую от BaseException и не является потомком Exception.

>>> def gen_f():
...     for i in range(100):
...         try:
...             yield i
...         except Exception:
...             print("exit")
... 
>>> gen = gen_f()
>>> next(gen), next(gen), next(gen)
(0, 1, 2)
>>> gen.close()
>>> next(gen, "EMPTY")
'EMPTY'

Мы видим, что строка "exit" не напечатана — значит, исключение не было обработано. Необработанное исключение привело к аварийному выходу из функции. Это завершило генератор, чего мы и добивались, вызывая метод close. При этом исключение не возбудилось в вызывающем контексте, так как его перехватил объект-генератор.

То есть, без веской на то причины, нет необходимости специально обрабатывать исключение GeneratorExit в генераторной функции.

У внимательного читателя может возникнуть вопрос: зачем вызывать метод close, если исключение GeneratorExit можно возбудить посредством throw метода?

Потому что это не одно и то же2:

  • Исключение, возбужденное методом throw, не перехватывается объектом-генератором.

  • Оно не завершает генератор, и последующая выдача элементов не приведёт к RuntimeError’у.

>>> gen = (i for i in range(10))
>>> gen.throw(GeneratorExit())  # не перехватывается генератором
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
GeneratorExit
>>> def gen_f():
...     for i in range(100):
...         try:
...             yield i
...         except GeneratorExit:
...             print("exit")
... 
>>> gen = gen_f()
>>> next(gen)
0
>>> gen.throw(GeneratorExit())  # не запрещает генерацию новых значений
exit
1
>>> next(gen)
2

И еще. Метод close вызывается при удалении генератора. Это поведение описано в PEP 342 - Coroutines via Enhanced Generators:

Add support to ensure that close() is called when a generator iterator is garbage-collected.

А здесь мы это проверим

>>> def gen_f():
...     try:
...         yield
...     except GeneratorExit:
...         print("I'm deleted")
... 
>>> gen = gen_f()
>>> next(gen)
>>> del gen
I'm deleted

Значение, возвращаемое методом close

Начиная с версии Python 3.13, метод close позволяет вернуть значение из генераторной функции. Для этого, после перехвата исключения GeneratorExit, значение нужно вернуть посредством инструкции return.

Убедимся в этом на примере.

Перепишем функцию string_summarizer так, чтобы генератор, получая строки через метод send, возвращал их конкатенацию в close методе.

def string_summarizer():
    strings = []
    try:        
        while True:        
            string = yield
            if isinstance(string, str):
                strings.append(string)
    except GeneratorExit:
        print("exit with strings =", strings)
        return ",".join(strings)            

gen = string_summarizer()
next(gen)  # Запускаем генератор
gen.send("cat")
gen.send("dog")
gen.send("rabbit")
print("close:", gen.close())

import sys
print(sys.implementation.name, sys.version)

Запустим скрипт в Python 3.13

exit with strings = ['cat', 'dog', 'rabbit']
close: cat,dog,rabbit
cpython 3.13.6 (main, Aug  8 2025, 16:06:35) [GCC 11.4.0]

А вот листинг выполнения для Python 3.12

exit with strings = ['cat', 'dog', 'rabbit']
close: None
cpython 3.12.11 (main, Jun  4 2025, 08:56:18) [GCC 11.4.0]

Как мы видим, начиная с версии Python 3.13, мы можем задавать возвращаемое методом close значение.

Итоги

Мы можем создать генератор, используя как генераторную функцию, так и генераторное выражение.

Генератор реализует протокол итератора, а также предоставляем методы send, close и throw.

Метод send

Метод send производит шаг итерирования и возвращает выданное генератором значение.
Параметр метода send становится значением yield выражения, передавшего управление.
Для вновь созданного генератора передача в метод send значения, отличного от None, приводит к возникновению исключения TypeError.
Генераторные выражения игнорируют переданные в send значения, кроме вызова send для вновь созданного генератора — там, также, значение, отличное от None, возбуждает TypeError.
Вызов метода send для генератора, завершившего свое выполнение, инициирует исключение StopIteration.
Выражения next(gen) и gen.send(None) эквивалентны для объекта-генератора.

Метод throw

Метод throw возбуждает исключение в генераторе.
Если такое исключение не обработать, оно распространится в вызывающий контекст и исчерпает генератор.
Для активной генераторной функции исключение, переданное в метод throw, возбуждается yield выражением, передавшим управление.
В случае обработки исключения, метод throw вернет следующий элемент генератора, выдаваемый генераторной функцией; если же следующего элемента не будет, то возникнет StopIteration исключение.
Генераторные выражения не обрабатывают исключения.
Генераторная функция не может обработать исключение, если она еще не запущена (вновь созданный генератор) или уже завершена (генератор исчерпал значение).

Метод close

Вызов метода close приводит к исчерпанию генератора.
Функция-генератор может обработать вызов метода close, перехватив исключение GeneratorExit, которое он возбуждает в генераторе.
Исключение GeneratorExit наследуется напрямую от BaseException и не является потомком Exception.
Нет необходимости обрабатывать исключение GeneratorExit без веской причины — в таком случае его перехватит породивший его объект генератора.
Генераторной функции запрещено выдавать новые элементы генератора после перехвата GeneratorExit . Она должна завершить своё выполнение.
Выражения gen.close() и gen.throw(GeneratorExit) — это совсем не одно и то же, они ведут себя совершенно по-разному.
При удалении генератора (в том числе при сборке мусора), Python вызывает для него метод close.
Начиная с версии Python 3.13 метод close возвращает значение, возвращаемое генераторной функцией в инструкции return.

Cпасибо тем, кто дочитал статью до конца. Буду признателен за конструктивную критику в комментариях


1 Нужно признать, что в референсе все-таки определена yield инструкция (yield statement). Она семантически эквивалентна yield выражению и синтаксически от него неотличима. Я искренне считаю, что её можно убрать — и референс от этого станет только лучше.

2 В разделе Generator-iterator methods референса языка утверждается, что close() и throw(GeneratorExit) эквивалентны:

generator.close()
Raises a GeneratorExit exception at the point where the generator function was paused (equivalent to calling throw(GeneratorExit)).

Как мы увидим, это совсем не одно и то же.

Комментарии (1)


  1. pavlushk0
    14.08.2025 06:25

    Интересная КДПВ - код одинаковый а знчки разные) Чему это должно нас научить?