Уведомления

Группа в Telegram: @pythonsu

#1 Апрель 14, 2010 13:05:08

r1der
От:
Зарегистрирован: 2010-04-13
Сообщения: 12
Репутация: +  0  -
Профиль   Отправить e-mail  

[Question] Что за кулисами yield, итераторов и генераторов?

1. Итераторы. Допустим стандартный типа list возвращает итератор, в чем заключается его оптимизация по скорости и памяти? Ведь список все равно храниться в памяти? То есть на самом деле геренерируется класс которые имеет метод next и запоминает какой последний элемент отдавал? Выходит так? Или я где то заблуждаюсь?
2. Аналогично. Что с генераторами? Ведь генераторы допустим списков возвращают список, в чем его оптимизация? Может кто нибудь объяснить?
3. yield Как внутри реализуются отложенные вычисления ? Что генерируется? Как посмотреть внутренности? Дизассемблировать не предлагать) Слишком много кода придется разобрать)



Офлайн

#2 Апрель 14, 2010 14:20:36

Ferroman
От:
Зарегистрирован: 2006-11-16
Сообщения: 2759
Репутация: +  1  -
Профиль   Отправить e-mail  

[Question] Что за кулисами yield, итераторов и генераторов?

Зачем дизассемблить? Можно просто посмотреть в исходниках. А вообще - просто строится цепочка ссылок на функцию, как я понимаю.

Офлайн

#3 Апрель 14, 2010 17:14:08

Андрей Светлов
От:
Зарегистрирован: 2007-05-15
Сообщения: 3137
Репутация: +  14  -
Профиль   Адрес электронной почты  

[Question] Что за кулисами yield, итераторов и генераторов?

На самом деле вопрос достаточно интересный (по крайней мере для того, чтобы на него ответить).

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 коде.



Офлайн

#4 Апрель 14, 2010 23:44:17

r1der
От:
Зарегистрирован: 2010-04-13
Сообщения: 12
Репутация: +  0  -
Профиль   Отправить e-mail  

[Question] Что за кулисами yield, итераторов и генераторов?

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

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



Офлайн

#5 Апрель 20, 2010 01:09:21

Андрей Светлов
От:
Зарегистрирован: 2007-05-15
Сообщения: 3137
Репутация: +  14  -
Профиль   Адрес электронной почты  

[Question] Что за кулисами yield, итераторов и генераторов?

Извините за задержку.
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
>>>



Отредактировано (Апрель 20, 2010 01:13:09)

Офлайн

Board footer

Модераторировать

Powered by DjangoBB

Lo-Fi Version