Найти - Пользователи
Полная версия: [Question] Что за кулисами yield, итераторов и генераторов?
Начало » Python для экспертов » [Question] Что за кулисами yield, итераторов и генераторов?
1
r1der
1. Итераторы. Допустим стандартный типа list возвращает итератор, в чем заключается его оптимизация по скорости и памяти? Ведь список все равно храниться в памяти? То есть на самом деле геренерируется класс которые имеет метод next и запоминает какой последний элемент отдавал? Выходит так? Или я где то заблуждаюсь?
2. Аналогично. Что с генераторами? Ведь генераторы допустим списков возвращают список, в чем его оптимизация? Может кто нибудь объяснить?
3. yield Как внутри реализуются отложенные вычисления ? Что генерируется? Как посмотреть внутренности? Дизассемблировать не предлагать) Слишком много кода придется разобрать)
Ferroman
Зачем дизассемблить? Можно просто посмотреть в исходниках. А вообще - просто строится цепочка ссылок на функцию, как я понимаю.
Андрей Светлов
На самом деле вопрос достаточно интересный (по крайней мере для того, чтобы на него ответить).

lorien уже рассказал про экономию памяти.

Теперь о том, как оно работает. Технически в CPython есть два стека: С и Python.
C стек используется как обычно - для вызова С функций (CPython написан на С, верно?)

Python стек состоит из объектов frame - и в них лежит информация о локальных переменных плюс еще несколько деталей.
Еще одно важное понятие - code object. Это байткод функции + служебная информация.
Итого: функция = code object + имя функции + значения параметров по умолчанию + не важные сейчас атрибуты.
Исполняемая функция = frame + code object.
Отступление: метод класса = класс + экземпляр класса + функция, смотрите выше.

Так вот, когда имеем дело с обычными питоновскими функциями, при их вызове создается frame, дальше функция работает на этом кадре и по выходу из функции (return, конец, исключение) - кадр уничтожается.

Если имеем генератор (функция с хотя бы одним yield statement внутри) - картинка немного другая. При вызове генератора создается объект-генератор. Внутри которого, есть frame и code object. На yield происходит выход из генератора наружу - но frame остается, он живет внутри generator object. И при следующем вызове .next (.send, .throw, .close) кадр не создается заново, а используется тот, который был сохранен в генераторе.

Та практике это выглядит так: при втором заходе в генератор имеем то же состояние, какое было на момент выхода. Код продолжается с yield, переменные сохранены. Очень удобно и здорово.

Я описал все несколько утрированно, опуская несущественные с моей точки зрения детали. Если нужно - можно показать все внутренности как они есть, на стороне Питона и в CPython коде.
r1der
В самих исходниках достаточно запутано все, много всего что не относиться к теме. Я понимаю что что создается кадр и как работают ленивые вычисления, мне интересно именно как это реализуется, в частности с итератором допустим.
xrange не создает список он просто запоминает последнее значение и увеличивает его на 1.. видимо так? То есть он за кулисами генерирует класс?
С yield'ом сложнее, какой класс должен быть сгенерирован чтобы yield запоминал где остановилось выполнение функции? Видимо в это кадре так называемом есть какой то атрибут который останавливает интерпретацию и запоминает где остановили, а как это выглядит внутри?

И еще вопрос, если использовать Shedskin он может правдоподобно показать что происходит внутри? То есть он генерирует оптимизированный код похожий на то что происходит внутри интерпретатора?
Андрей Светлов
Извините за задержку.
Shed не использовал никогда, но “правдоподобно показать что происходит внутри”, имхо, он не сумеет.

Давайте просто из самого питона посмотрим для начала
Примитивный генератор:
>>> def f():
... for i in range(10):
... yield i
Запустим его и посмотрим, что вернет:
>>> i = f()
>>> dir(i)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', __repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']
>>> i.gi_code
<code object f at 0x22fce40, file "<input>", line 2>
>>> i.gi_frame
<frame object at 0x2422c30>
>>> i.next()
0
>>> i.next()
1
>>> i.gi_frame
<frame object at 0x2422c30>
фрейм - тот же самый объект.
Давайте глянем, что у него внутри:
>>> dir(i.gi_frame)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'f_back', 'f_builtins', 'f_code', 'f_exc_traceback', 'f_exc_type', 'f_exc_value', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_restricted', 'f_trace']
Самое интересное на данный момент - локальные переменные:
>>> i.gi_frame.f_locals
{'i': 1}
>>> i.next()
2
>>> i.gi_frame.f_locals
{'i': 2}
>>>
>>> i.gi_frame.f_lasti
22
Видно, как они меняются.
i.gi_frame.f_lasti - адрес последней выполненной инструкции. В этом примере она, понятное дело, всегда будет 22 - yield же один единственный и генератор всегда останавливается на одном и том же месте (только до первого i.next() там лежит -1 - генератор не начинает работу автоматически после создания, его нужно дернуть один раз).

Чуть более сложный пример:
>>> def g():
... for i in range(10):
... if i % 2:
... yield i
... else:
... yield i
...
>>> j = g()
>>> j.gi_frame.f_lasti
-1
>>> j.next()
0
>>> j.gi_frame.f_lasti
42
>>> j.next()
1
>>> j.gi_frame.f_lasti
33
>>> j.next()
2
>>> j.gi_frame.f_lasti
42
>>> j.next()
3
>>> j.gi_frame.f_lasti
33
>>>
Теперь, если требуется, можно посмотреть в исходники питона и таки разобраться, как генератор работает с фреймом.

P.S. xrange - это не генератор, а такой себе класс. Поддерживающий протокол итератора и контейнера, и считающийся лениво
>>> z = xrange(10)
>>> z
xrange(10)
>>> type(z)
<type 'xrange'>
>>> dir(z)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__getitem__', '__hash__', '__init__', '__iter__', '__len__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> z[5]
5
>>> z[9]
9
>>> z[3]
3
>>>
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB