Получить значение из вложенной генераторной функции, возвращённое с помощью оператора "return"

Вот мой код:

def gen_middle():
    def gen_nested():
        counter = 0
        while True:
            yield counter
            counter += 1
            if counter >= 2:
                return 'My special value'

    yield from gen_nested()

def main():
    iterator = iter(gen_middle())
    
    try:
        while True:
            value = next(iterator)
            print(value)
    except StopIteration as exc:
        # returned value (with "return") from gen_nested was expected
        print(exc.value)

main()

Я ожидал вывод:

0
1
My special value

Но получил:

0
1
None

В связи с этим у меня два вопроса.

  1. Оператор yield from не эскалирует исключение StopIteration со значением из вложенного генератора?
  2. Могу ли я получить это значение ('My special value') не меняя код функций gen_middle и gen_nested?

Ответы (1 шт):

Автор решения: insolor

Все высокоуровневые итерирующие конструкции (от for до yield from) сами обрабатывают StopIteration, наружу "выходит" уже новое исключение StopIteration, а то что было брошено самым внутренним итератором со своим value наружу не выходит. Наверное, логично было бы, чтобы value пробрасывалось через всю цепочку отловленных StopIteration, но видимо это такой редкий случай (use case), что никто про это не подумал.

С другой стороны, если внутри было несколько итераторов (например, происходит склейка нескольких последовательностей значений, как в itertools.chain), то какое из значений должно пробрасываться наружу? Если последнее, то почему оно, почему должны пропасть return значения других генераторов?

Тут проще всего создать свое отдельное исключение, изнутри его выбрасывать, на самом внешнем уровне обрабатывать, тогда и внешний while-next можно заменить на обычный for (что более красиво):

class MyCustomException(Exception):
    def __init__(self, value):
        self.value = value


def gen_middle():
    def gen_nested():
        counter = 0
        while True:
            yield counter
            counter += 1
            if counter >= 2:
                raise MyCustomException('My special value')

    yield from gen_nested()


def main():
    iterator = iter(gen_middle())
    
    try:
        for value in iterator:
            print(value)
    except MyCustomException as exc:
        print(exc.value)


main()

Конкретно для случая, если внутри функции ровно один yield from, можно его тупо заменить на return, чтобы просто возвращался исходный генератор:

class MyCustomException(Exception):
    def __init__(self, value):
        self.value = value


def gen_middle():
    def gen_nested():
        counter = 0
        while True:
            yield counter
            counter += 1
            if counter >= 2:
                return 'My special value'

    return gen_nested()  # было yield from


def main():
    iterator = iter(gen_middle())
    
    try:
        while True:
            value = next(iterator)
            print(value)
    except StopIteration as exc:
        # returned value (with "return") from gen_nested was expected
        print(exc.value)


main()
→ Ссылка