Модуль 7 · Итераторы и генераторы

Справочник по Python · модуль 7 · итераторы и генераторы

Глава 7.5

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Пример: вверх и вниз

Два простых генератора — счёт вверх и счёт вниз — и третий, составленный из них.

main.pyделегирование двум генераторам подряд
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) так:

main.pyтот же результат, больше кода
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 xyield from gen
две строки на каждый источникодна строка
заводится лишняя переменнаяпеременная не нужна
передаёт только значенияпередаёт также send(), исключения и close()
результат подгенератора теряетсярезультат доступен через присваивание

Для простого перебора обе записи равносильны. Различия из последних двух строк проявляются в сопрограммах из главы 7.3 — там ручной цикл уже не эквивалент.

§7Источником может быть любой итерируемый объект

После yield from необязательно стоит генератор — годится всё, что перебирается.

main.pyсписок, кортеж, строка, словарь
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 — когда генератор делегирует самому себе. Так разворачивают вложенные списки любой глубины.

main.py«выпрямление» вложенных списков
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 его подхватывает.

main.pyвнутренний генератор возвращает итог
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 5TypeError: число не перебираетсяпередавать итерируемый объект
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() и исключения и забирает значение, возвращённое подгенератором. Как только значения нужно обрабатывать, а не просто пропускать, вместо неё пишут обычный цикл.

Глава 7.5 · Делегирование: yield from · Справочник по Python Автор: Макаров Максим Николаевич