Справочник по Python · модуль 7 · итераторы и генераторы
Глава 7.5
Делегирование:
yield from
Функция с yield никогда не работает сама по себе — ею всегда управляет внешний код. Поэтому вызвать один генератор из другого просто так нельзя. Команда yield from передаёт ему управление и все значения, избавляя от ручного цикла-посредника.
- Категория
- Генераторы
- Уровень
- Продвинутый
- Связанные темы
- yield, итераторы, рекурсия
- Зачем это знать
- Собирать генераторы из генераторов без лишнего кода
§1Генератор не выполняется сам
Неотъемлемое свойство генераторов в том, что функция с yield никогда не выполняется сама по себе. Ею всегда должен управлять другой код: цикл for или явные вызовы next(). Без такого управления тело функции просто не начинает работать.
Для отдельного генератора это привычно. Но как только генератор понадобилось собрать из других генераторов, свойство превращается в неудобство: вызова функции-генератора недостаточно, чтобы она заработала.
§2Что происходит при простом вызове
def up_and_down(n): countup(n) # создан объект генератора — и выброшен countdown(n) # то же самое
Каждая строка лишь создаёт генераторный объект и тут же его теряет. Ни одного значения наружу не выйдет, а сама up_and_down вообще не будет генератором — в её теле нет yield.
§3Команда yield from
Решает задачу команда yield from: она делегирует перебор — передаёт управление внутреннему генератору и выпускает наружу все его значения, одно за другим, как если бы их выдавал внешний генератор.
yield from итерируемый_объект
§4Пример: вверх и вниз
Два простых генератора — счёт вверх и счёт вниз — и третий, составленный из них.
def countup(stop): n = 1 while n <= stop: yield n n += 1 def countdown(start): n = start while n > 0: yield n n -= 1 def up_and_down(n): yield from countup(n) yield from countdown(n) for x in up_and_down(5): print(x, end=' ')
1 2 3 4 5 5 4 3 2 1
Снаружи виден один генератор, выдающий десять значений. Кто именно их произвёл, потребителю знать не нужно.
§5Как это выглядит без yield from
yield from избавляет от необходимости управлять перебором вручную. Иначе пришлось бы записать up_and_down(n) так:
def up_and_down(n): for x in countup(n): yield x for x in countdown(n): yield x
Цикл здесь — чистый посредник: он получает значение и немедленно отдаёт его дальше, не делая с ним ничего. Именно такой цикл yield from и заменяет.
§6Сравнение двух записей
for x in gen: yield x | yield from gen |
|---|---|
| две строки на каждый источник | одна строка |
| заводится лишняя переменная | переменная не нужна |
| передаёт только значения | передаёт также send(), исключения и close() |
| результат подгенератора теряется | результат доступен через присваивание |
Для простого перебора обе записи равносильны. Различия из последних двух строк проявляются в сопрограммах из главы 7.3 — там ручной цикл уже не эквивалент.
§7Источником может быть любой итерируемый объект
После yield from необязательно стоит генератор — годится всё, что перебирается.
def chain_all(): yield from [1, 2] yield from (3, 4) yield from "ab" yield from {"k": 5} print(list(chain_all()))
[1, 2, 3, 4, 'a', 'b', 'k']
Правила те же, что у цикла for из главы 7.1: перебирается всё, что поддерживает протокол итератора.
§8Рекурсия: разбор вложенной структуры
Самое сильное применение yield from — когда генератор делегирует самому себе. Так разворачивают вложенные списки любой глубины.
def flatten(items): for item in items: if isinstance(item, list): yield from flatten(item) else: yield item print(list(flatten([1, [2, [3, [4, 5]], 6], 7])))
[1, 2, 3, 4, 5, 6, 7]
Без делегирования такую функцию пришлось бы писать заметно длиннее: каждый уровень вложенности требовал бы своего цикла-посредника. Здесь же вся рекурсия умещается в одну строку.
§9Что ещё делегируется
yield from передаёт вниз не только запросы значений. Пока внешний генератор стоит на этой строке, между вызывающим кодом и внутренним генератором устанавливается прямой канал.
| Действие снаружи | Куда попадает |
|---|---|
next() | во внутренний генератор |
send(value) | во внутренний генератор |
close() | закрывает внутренний, затем внешний |
исключение через throw() | возбуждается внутри внутреннего |
Внешний генератор на это время становится прозрачным. Ручной цикл for x in gen: yield x такой прозрачности не даёт: присланные значения дошли бы только до него и дальше не пошли.
§10Возвращаемое значение подгенератора
Генератор может завершиться командой return со значением. При обычном переборе это значение недоступно, но yield from его подхватывает.
def inner(): total = 0 while True: value = yield if value is None: return total total += value def outer(): result = yield from inner() print("подгенератор вернул:", result) yield result gen = outer() next(gen) gen.send(5) gen.send(7) print(gen.send(None))
подгенератор вернул: 12 12
Запись result = yield from inner() читается так: отдавать наружу всё, что выдаёт inner, а когда тот завершится — забрать его возвращённое значение. Это и делает yield from механизмом делегирования, а не просто сокращением цикла.
§11Когда нужен обычный цикл
yield from подходит только тогда, когда значения передаются без изменений. Как только с ними нужно что-то сделать — отфильтровать, преобразовать, пронумеровать — возвращается обычный цикл.
yield from countup(n) всё как есть
for x in countup(n): только чётные,
if x % 2 == 0: и удвоенные
yield x * 2
§12Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
countup(n) отдельной строкой | генератор создан и потерян, значений не будет | yield from countup(n) |
yield countup(n) | наружу выйдет сам объект генератора, а не его значения | yield from countup(n) |
yield from 5 | TypeError: число не перебирается | передавать итерируемый объект |
return countup(n) внутри генератора | завершит генератор, значения не выйдут наружу | yield from |
ждать от yield from изменения значений | они передаются как есть | писать цикл с обработкой |
| цикл-посредник в сопрограмме | send() не дойдёт до внутреннего генератора | yield from устанавливает прямой канал |
§13Проверьте себя
Почему нельзя просто вызвать генератор внутри другого генератора?
Вызов лишь создаёт генераторный объект. Функция с yield не выполняется сама — ею должен управлять внешний код.
Какой цикл заменяет yield from?
Цикл-посредник for x in gen: yield x, который получает значение и сразу отдаёт его дальше.
Что может стоять после yield from?
Любой итерируемый объект: генератор, список, кортеж, строка, словарь.
Чем yield from отличается от цикла-посредника?
Он передаёт вниз ещё и send(), close() и исключения, а также забирает значение, возвращённое подгенератором через return.
Как получить возвращённое подгенератором значение?
Записать результат делегирования: result = yield from inner().
§14Шпаргалка
yield from generator()yield from [1, 2, 3]yield from flatten(item)result = yield from inner()for x in gen: yield xИтог главы
Функция с yield не работает без внешнего управления, поэтому вызвать один генератор из другого напрямую нельзя. yield from делегирует перебор: передаёт управление внутреннему источнику и выпускает его значения наружу без изменений. Заодно она заменяет цикл-посредник, поддерживает рекурсию в одну строку, пропускает вниз send(), close() и исключения и забирает значение, возвращённое подгенератором. Как только значения нужно обрабатывать, а не просто пропускать, вместо неё пишут обычный цикл.