1

Столкнулся с очень странным выводом при запуске кода:

def magic():
    raise RuntimeError
    print("bar")
    yield 100

print(magic())

и будет неожиданный для меня ответ:

<generator object gen at 0x7fb3073f0f68>

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


потому как тип у генератор не отличается от обычной функции

def no_magic():
    pass

type(magic) is type(no_magic)

gil9red
  • 77,085
devnull
  • 1,219
  • 12
  • 38

2 Answers2

6

Схематично работа генератора выглядит так.

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

При вызове next с параметром возвращенным вызовом magic() запускается собственно код magic и когда доходит до yield, то в этом месте значение, которое передано как аргумент в yield вернется из next. Вот пример с разметкой шагов, которые показывают порядок выполнения:

# я изменил функцию, чтобы продемонстрировать работу yield
def magic():
    yield 100  # шаг 3
    yield 200  # шаг 7
    print(1)   # шаг 11
    yield 300  # шаг 12
    print(2)   # шаг 16

gen = magic() # шаг 1 a = next(gen) # вызов next - шаг 2, присвоение - шаг 4 print(a) # шаг 5, выведет в консоль 100 a = next(gen) # вызов next - шаг 6, присвоение - шаг 8 print(a) # шаг 9, выведет в консоль 200 a = next(gen) # вызов next - шаг 10, присвоение - шаг 13 print(a) # шаг 14, выведет в консоль 300 next(gen) # вызов шаг 15, исключение StopIteration - шаг 17 # т.к. magic закончила выполнение

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

Что касается типов, то тип magic и no_magic тот же - function. Важно что возвращает функция:

>>> type(magic())
<type 'generator'>
>>> type(no_magic())
<type 'NoneType'>

Сам код no_magic выполняется непосредственно в точке вызова no_magic(), у magic() этого не происходит, вызов отложен или еще по другому говорят, что вычисления делаются "лениво" или "по требованию"(lazy или on demand).

4

Описание:

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


Генераторы возвращают значение через оператор yield.

А так вы генератор вы создали через magic(), но вы его не выполнили.

Например так:

def magic():
    raise RuntimeError
    print("bar")
    yield 100

gen = magic() print(gen) print(next(gen))

gil9red
  • 77,085
  • спасибо, но можно развернуть ответ иобявнить как это реализованно? интересует реализация, удивило что наличие yield так сильно мменяет повидение функции – devnull Dec 04 '20 at 14:13
  • даже еслизаменить raise на return или exit поведения не измениться – devnull Dec 04 '20 at 14:14
  • @devnull, об этом лучше расскажут книги :) Добавил вырезку в ответ про генераторы – gil9red Dec 04 '20 at 14:15
  • есть рекоминдации по книжкам? : ) не находил которые хорошо раскрывают тему как Python работает с памятьсю и как создает там обекты типа функции или генераторов – devnull Dec 04 '20 at 14:17
  • @devnull, у нас есть по этому вопросу специальная тема: https://ru.stackoverflow.com/questions/420125/ :) – gil9red Dec 04 '20 at 14:18
  • к сожалению не увидел там про пизкоуровневую реализацию в python – devnull Dec 04 '20 at 14:30
  • @devnull, вряд ли есть книги про низкоуровневую реализацию. Для этого смотрите в реализации интерпретаторов питона, например https://github.com/cython/cython и https://github.com/python/cpython – gil9red Dec 04 '20 at 14:36