В статье 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 aGeneratorExit
exception at the point where the generator function was paused (equivalent to callingthrow(GeneratorExit)
).
Как мы увидим, это совсем не одно и то же.
Комментарии (2)
Andrey_Solomatin
14.08.2025 06:25Cпасибо тем, кто дочитал статью до конца. Буду признателен за конструктивную критику в комментариях
Долистал. Про эту функциональность у меня всёгда возникает вопрос а зачем мне это.
Я так ни разу send и не использовал для нормальных задач. Так поиграться сделать свою версию асинхронности на герераторах.
pavlushk0
Интересная КДПВ - код одинаковый а знчки разные) Чему это должно нас научить?