среда, января 13, 2016

CPython для самых маленьких: объекты и их атрибуты

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

Центральная идея
В свое время меня удивило то, что многие решения, наблюдаемые в современном питоне были приняты в очень бородатые годы. Знакомьтесь, это CPython 1.0.1, для желающих его собрать вот инструкция. Уже тогда BDFLу было понято не только то, что питон должен быть ООП языком с классами (пользовательские классы были тогда совсем другими, впрочем), но и то, как это сделать, причем реализация в корне не изменилась до сих пор. Заключается она вот в чем - все объекты, операбельные питоном, расширяют тип PyObject, который грязные хиппи девяностых (вроде так всё было) называли просто object, а потом переименовали из-за конфликта имен с чем-то виндовым. Сейчас он выглядит так:
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
Вот такой крошечный структ на два поля, макрос _PyObject_HEAD_EXTRA в релизных сборках обычно разворачивается в ничто. Поле ob_refcnt - это счетчик ссылок на этот объект, используется в gc, ob_type - ссылка на тип объекта. Тип имеет смысл описателя поведения объекта, и сам по себе он не обязан быть чем то питоновским, типы могли бы быть реализованы как чисто сишный хардкод где то в недрах интерпретатора. Но если бы это было так, программирование на питоне стало бы довольно скучным занятием - даже простое наследование стало бы невозможным. В результате инсайт заключается в том, что мы скорее договорились до объктно-классовой интерпретации, в то время как на самом деле есть только объекты (всевозможные расширения PyObject) ссылающиеся на другие объекты (и даже на себя) как на типы. Поскольку у всех объектов по определению есть тип, и все типы нарочно реализованы как объекты мы естественно приходим к знаменитому "в питоне всё - объект некоторого класса".

Задраить люки!
Писать статью дальше было довольно сложно. Штука в том, что исследуя что-то для себя новое ты неизбежно проходишь множество тупиковых путей. Понятное дело, что такое изучение может затянуться на значительное время, зато в результате картина исследуемого объекта становится более объемной. И вот однажды ты думаешь "хэй, да я же могу написать об этом статью в бложике" и удивляешься тому, насколько сложно проложить для другого человека дорогу вдоль которой изучаемый объект делал бы максимум смысла, если вы понимаете о чем я. В твоей копилке просто нет такого пути, и всё что ты можешь сделать - это построить его плохонькую аппроксимацию. Как и в прошлый раз в качестве такой аппроксимации мы просто что-нибудь запустим и попытаемся расслышать шепот из под капота CPython.

Теперь, когда фундаментальные основы слоя объектов установлены мы можем рассмотреть отношения объект-класс в непосредственной близости. Мы ещё не касались тонкостей анатомии объектов (да и не будем в этот раз, как-то это утомительно), пока нам достаточно того, что это структы, расширяющие PyObject. Давайте рассмотрим поиск атрибута у объекта. Дизассемблируем простой лукап, вроде foo.bar
>>> import dis
>>> dis.dis("foo.bar")
  1           0 LOAD_NAME                0 (foo)
              3 LOAD_ATTR                1 (bar)
              6 RETURN_VALUE
В момент исполнения по имени foo интерпретатор попытается загрузить на связанный с текущим фреймом value stack некоторый объект. Для этого сначала будет просмотрен словарь локального окружения, затем глобального и, напоследок, встроенного (builtin). У каждого фрейма эти словари свои. Если такой лукап не окончился ошибкой, то далее будет выполнен опкод LOAD_ATTR, вот соответствуюший отрывок из PyEval_EvalFrameEx:
TARGET(LOAD_ATTR) {
    PyObject *name = GETITEM(names, oparg);
    PyObject *owner = TOP();
    PyObject *res = PyObject_GetAttr(owner, name);
    Py_DECREF(owner);
    SET_TOP(res);
    if (res == NULL)
        goto error;
    DISPATCH();
}
Из специального массивчика имен, который лежит в объекте кода и доступен в питоне как атрибут co_names, по индексу 1 (см. дизассемблерный листинг) забирается строчка "bar", после чего она "лукапится" у объекта owner специализированной C-API функцией PyObject_GetAttr.  Она является частью abstract objects layer. Этот "слой" (логический слой C-API) представлен шестью группами функций или, как их называет дока, протоколами. За счет использования протоколов стековая машина в значительной мере изолирована от слоя объектов. Реализация этой функции так же довольно абстрактна: берем тип объекта (переменная tp) и уже у него просим достать из объекта атрибут.
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
    PyTypeObject *tp = Py_TYPE(v);
 
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
 
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}
Важно понимать, что такое делегирование запроса в тип не является особенностью именно этой API функции. Объект выражает лишь конкретное значение, в то время как его поведение определено в типе.
Все типы - это расширения базового типа PyTypeObject, который, конечно, тоже является PyObject`ом. Так, класс всех классов type - это лишь очередная реализация PyTypeObject, наряду с остальными, причем его PyObject->ob_type смотрит на себя, так что он сам себе класс. Шаблон, которым и является PyTypeObject - это здоровенный структ, состоящий преимущественно из указателей на сишные функции. В терминах местной тусовки подобные члены структа называются слотами. Слоты - любопытнейшая штука, многие из них, хотя и не все, отображаются в некоторый magic атрибут класса, навроде __call__, __new__ или __add__. Магия заключается в том, что несмотря на их низкоуровневость, слотами можно рулить из питона, причем в динамике:
class A: pass
>>> A.__call__ = lambda self: print('HelloWorld')
>>> A()()
HelloWorld
>>> A.__call__ = lambda self: print('Some other text, maybe')
>>> A()()
Some other text, maybe
Как именно это происходит мы будем говорить в следуюший раз, для этого нам понадобится пониманием механизмов, стоящих за встроенным метаклассом type. Вернемся к слоту tp_getattro, в питоне он отображается в атрибут __getattribute__, так что в общем случае его значение определено только в рантайме. Тем не менее у него есть стандартная реализация, которую использует большинство встроенных типов. Давайте поймаем её в отладчике, будем реверсить такой код:
a = A()
stop()  # мы обсудили эту функцию в прошлый раз
a.b
Сразу после SIGTRAP, сгенериррованного вызовом stop() ставим новый брейк
(gdb) b PyObject_GetAttr
Breakpoint 1 at 0x4cbb21: file Objects/object.c, line 864.
и даем интерпретатору до него добраться
(gdb) cont
В месте остановки (мы внутри PyObject_GetAttr в Си и на строчке a.b в питоне) мы имеем доступ к локальным переменным, возьмем тип объекта a
(gdb) p tp
(PyTypeObject *) 0x9f6458
По адресу 0x9f6458 лежит класс A, осталось лишь заглянуть ему под днище
(gdb) p ((PyTypeObject *) 0x9f6508)->tp_getattro
(getattrofunc) 0x4cc71d <PyObject_GenericGetAttr>
Итого видим, что дефолтовая реализация tp_getattro - это функция PyObject_GenericGetAttr, имеющая разочаровывающе предсказуемое имя. Чуть ниже мы рассмотрим её подробнее, оказывается, что её устройство имеет фундаментальные корни. Пока подведем мини-итог:
1. Объекты и их типы представлены одной и той же базовой сущностью PyObject. Связь между ними заключается в поле ob_type объекта и куче сишного хардкода в типе.
2. Поведение любого объекта всегда определяется его типом, даже если он сам является типом.
3. Каждый тип определяет некоторое множество слотов - функций, определяющих реакцию его инстансов на те или иные внешние раздражители.
Убедимся, что для написания простейших программ нам не нужен ни сам питон, ни стековая машина. Давайте вычислим значение выражения 123 + 456.
Создаем новые PyLongObject - объекты класса int в питоне, соответствуюшие числам 123 и 456:
(gdb) call PyLong_FromLong(123)
(PyObject *) 0x9836e0
(gdb) call PyLong_FromLong(456)
(PyObject *) 0x7ffff7f64e50
Теперь попросим объект PyLong_Type их сложить, дернув соответствующий слот:
(gdb) call PyLong_Type->tp_as_number->nb_add(0x9836e0, 0x7ffff7f64e50)
(PyObject *) 0x7ffff7f64ea0
В результате в памяти по адресу 0x7ffff7f64ea0 лежит результат - новый инстанс класса int, попринтим его что-ли:
(gdb) call PyObject_Print(0x7ffff7f64ea0, stderr, 1)
579
Похоже на то, что 123 + 456 = 579. Если эту задачу отдать типу PyComplex_Type, то в результате получится (579+0j), как и ожидается.

Атрибуты
Пример, рассмотренный выше, может показаться местячковым, ну подумаешь лукап всякого из объекта. Однако не стоит забывать, что любое вычисление - это чтение, обработка и запись  данных, которые в питоне хранятся в виде атрибутов объектов. Более того, оказывается, что эти механизмы неявно используются повсеместно, так без них невозможна правильная работа функций. Работа поиска атрибутов крутится вокруг результата функции _PyType_Lookup, на диаграмме ниже показан путь, по которому проходит её результат descr
azaza (3).png
Именно _PyType_Lookup реализует правильный обход линеаризации графа наследования (о том что это, поговорим в следующий раз). Результатом является указатель на питоновский объект или null. Дальше, согласно диаграмме, если тип объекта descr реализует слоты tp_descr_get & tp_descr_set (последнее будет проверено в is_data?), то такой объект нарекают дата дескриптором и возвращают результат работы его tp_descr_get слота. Если объект descr оказался null или не дата дескрипртором, то поиск атрибута происходит в дикте самого объекта. И только если атрибута в дикте не оказалось, будет возвращено значение слота tp_descr_get если он не null. Рассмотренный дата дескриптор является частной реализацией так называемого протокола дескрипторов. Произвольный класс, реализующий питоновские методы __get__, __set__ или __del__ называется дескриптором (описателем) атрибута. Наглядный пример работы дата дескрипторов представлен ниже.
Создаем класс, реализующий поведение дата дескриптора:
class DataDescriptor:
    def __get__(self, obj, klass=None):
        return "This came from descr_get."
    def __set__(self, obj, value):
        return "This is descr_set speaking."
Создаем пользователя этого дескриптора
class Foo:
    bar = DataDescriptor()
Атрибут bar в классе теперь контролируется дескриптором
>>> print(Foo.bar)
This came from descr_get.
так же как и в инстансе
>>> f = Foo()
>>> f.bar
This came from descr_get.
В этом инстансе мы, конечно, можем хранить другое значение для bar
>>> f.__dict__["bar"] = "This came from instance."
>>> f.__dict__['bar']
This came from instance.
Но PyObject_GenericGetAttr предпочитает этого не замечать
>>> f.bar
This came from descr_get.
Заметьте, что этот пример работает только для дата дескрипторов, т.е. помимо __get__ мы должны иметь ненулевой __set__. Выбросим последний и чуда не случится.
И тут мы вплотную подходим к самому интересному - к пониманию того, что дескрипторы являются универсальным клеем внутри питона, на котором висят все magic атрибуты и многое другое. Рассмотрим довольно типичный код:
>>> class SomeClass: pass
>>> s = SomeClass()
>>> s.foo = 10
>>> s.__dict__
{'foo': 10}
>>> s.__dict__['bar'] = 20
>>> s.bar
20
Не знаю как у вас, а у меня такое поведение вызывает массу вопросов, хотя бы вот:
1. как атрибуты, доступные через точку оказываются в дикте объекта;
2. почему изменяя какой то там дикт, пусть на него даже есть ссылка в объекте, мы изменяем атрибуты объекта;
3. что вообще такое __dict__ в конце концов, я такой атрибут не просил и почему s.__dict__ != SomeClass.__dict__, в то время как для других атрибутов ожидается противоположное. What kind of black sorcery is this?
Ну что же, %username%, думаю, ты уже все понял, __dict__ - это дескриптор, который нашла _PyType_Lookup в слоте tp_dict самого SomeClass. Чтобы копать дальше, хотелось бы взглянуть на __dict__ класса SomeClass, но мы уже поняли, что вокруг обман, так что для начала проверим слот как есть, SomeClass у меня лежит в 0x9f6808:
(gdb) call PyObject_Print(((PyTypeObject *) 0x9f6808)->tp_dict, stderr, 0)
{..., '__dict__': <attribute '__dict__' of 'SomeClass' objects>, ...}
Ключи этого дикта в точности совпадают с ключами в SomeClass.__dict__, хотя тип последнего и не dict, а некий mapping_proxy, это нас устраивает:
>>> SomeClass.__dict__['__dict__']
<attribute '__dict__' of 'SomeClass' objects>
>>> type(SomeClass.__dict__['__dict__'])
<class 'getset_descriptor'>
Получается что атрибут __dict__ инстансов класса SomeClass контролируется дескриптором getset_descriptor. Мы можем дернуть его __get__, чтобы получить __dict__ объекта s:
>>> SomeClass.__dict__['__dict__'].__get__(s, SomeClass)
{'foo': 10}
Достигается это за счет того, что внутри объекта s и правда есть ссылка на питоновский dict, лежащий где-то в памяти. Положение этой ссылки внутри объекта знает класс SomeClass - в слоте tp_dictoffset в явном виде лежит смещение внутри памяти объекта s в байтах, чем и пользуется getset_descriptor. Если бы в tp_dict класса SomeClass по ключу __dict__ был бы обычный питонячий __dict__, пункт 2 не мог бы быть правдой, давайте в этом тоже убедимся:
>>> import ctypes
>>> pointer = ctypes.cast(
      id(SomeClass)+type.__dictoffset__, ctypes.POINTER(ctypes.py_object)
    )
>>> pointer[0]
# pointer[0] это то, что лежит в tp_dict в явном виде, мы обошли стороной все трюки дескрипторов
{'__dict__': <attribute '__dict__' of 'SomeClass' objects>, ...}
Суем вместо дескриптора пустой дикт:
>>> pointer[0]['__dict__'] = {}
>>> s.__dict__['tar'] = "may there be light"
Видим, что отображение дикта в атрибуты больше не работает, чтд
>>> s.tar
AttributeError: 'SomeClass' object has no attribute 'tar'
Рассмотренное выше является лишь каплей в море, сами зацените остальные magic`и. Я понимаю, что эти технические подробности вряд ли будут полезны сами по себе. Задача этой секции заключалась в том, чтобы дать ощущение некой нелокальности CPython. Так, мы увидели, что попросить атрибут у объекта в питоне совсем не то же самое, что достать поле из сишного структа, где этот процесс очень прямолинейный. Напротив, даже простые действия в CPython разбиты на множество независимых ступеней, относящихся при этом к разным слоям абстракций, на каждом из которых реализацию можно подогнать под свои нужды.

Функции как дескрипторы
Рассмотрим еще более жизненный пример использования дескрипторов в CPython. Вы, должно быть, знаете о том, что PyFunc_Type (в питоне types.FunctionType - класс функций) реализует свойства дескрипторов. Нужно это для того, чтобы красиво вклинить реализацию методов объектов, методов классов, статических методов и пропертей в общую канву, наряду с простыми функциями. Вот отрывок из реализации класса функций
PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&amp;PyType_Type, 0)
    "function",
    ...
    func_descr_get,             /* tp_descr_get */
    0,                          /* tp_descr_set */
    ...
};
Видно, что реализована только часть протокола дескриптора - отсутствует set. Это значит, что в инстансе класса можно перекрыть метод:
class SomeClass:
    def foo(self):
        return "InClass implementation"
 
s = SomeClass()
s.foo = lambda self: "InInstance implementation"
 
>>> s.foo(s)
InInstance implementation
Обратите внимание, что s.foo баунд методом так и не стала - ей нужно явным образом передавать self. Теперь, когда мы знаем как работает PyObject_GenericGetAttr такое поведение становится самоочевидным. Идем дальше, заглянем в tp_descr_get функции.
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj);
}
Иными словами, если функцию попросили у типа (т.е. obj == Py_None), то в ответ мы получим её саму в первозданном виде. Именно поэтому методы в тройке (щас доберемся до модифицирующих декораторов) при лукапе из класса выглядят обычными функциями - они и есть обычные функции. Если же obj != Py_None, то возвращается PyMethodObject, его собирает PyMethod_New из функции func и инстанса obj. Слот tp_call типа метода оборочивает вызов фунции func, автоматически подставляя туда первым аргументом obj. Как только мы осознаем всё это, вопросы об устройстве декораторов вроде classmethod и пр. отпадают сами собой (ну мне так кажется). И да, classmethod - это класс, можете наследоваться от него. Более того, само его наличие в стандартной поставке решает, скорее, вопрос производительности, вы можете реализовать все фичи classmethod и компании на чистом питоне.
Теперь убедимся, что любая функция может быть использована как метод любого класса или объекта, например "приделаем" к десятке метод factorial
def factorial(self):
    return 1 if self == 1 else factorial(self-1) * self
 
ten_factorial = factorial.__get__(10, int)
Несмотря на всю нелепость происходящего, ten_factorial - это вполне себе нормальный метод объекта 10:
>>> ten_factorial
<bound method int.factorial of 10>
 
>>> ten_factorial()
3628800

Подведем итоги
1. Классы и их инстансы - это структы, расширяющие PyObject.
2. В результате этого все объекты питона без исключения имеют тип (класс).
3. Поведение любого объекта определяется его типом, один и тот же объект будет вести себя по-разному в зависимости от навязанного типа (это можно сделать в gdb).
4. Высокоуровневое поведение объектов весьма неявно мапится в их низкоуровневую реализацию. Вы никогда не видите объекты такими, какие они есть на самом деле, любое представление объекта в питоне - это результат работы какого то кода (возможно очень развесистого).
5. Большую роль в жизни объектов играют дескрипторы, которым делегируется взаимодействие с атрибутами.

В следующие разы, мы обсудим работу наследования типов, работу метаклассов, super, флаги классов - tp_flags, роль класса object и мб что-то ещё, пишите в коментариях. Надеюсь это было кому то полезно, однако тут без личного погружения в пучины вод не разобраться. Спасибо за ваше время.

суббота, апреля 25, 2015

CPython для самых маленьких: введение

Этой статьей мне хотелось бы открыть цикл, посвященный внутреннему устройству интерпретатора языка Python. Большую часть времени мы будем говорить о коде CPython версии не ниже чем 3.3, который собран и запущен на Linux based OS. Цель этого цикла – показать, что питоновские внутренности - это совсем не страшно (я про интерпретатор), в них может разобраться любой желающий, при этом профит, который вы получаете, выражается если даже не в повышении зарплаты и интереса к вам со стороны женщин, то по меньшей мере в собственном удовлетворении и возможности повыделываться перед коллегами в курилке.


Подготовительный этап

Ок, предположим, что вы тем или иным образом завладели кодом CPython. Если вы собираетесь экспериментировать с этими исходниками, то вот вам небольшой совет – если вы скачали tarball, рекомендую инициализировать внутри git репозиторий, добавить в .gitignore те бинари, которые произведет компилятор в процессе сборки и закоммитить это ванильное состояние, на которое, если что, можно сделать reset. Стандартный скрипт configure нужно будет запустить с ключем --with-pydebug, это поможет нам в дальнейшем, и, напоследок, полезно прогнать тесты. В результате должен получиться полностью работоспособный интерпретатор:
magniff@/home/magniff/Downloads/Python-3.4.2$ ./python 
Python 3.4.2 (default, Apr 10 2015, 20:16:23) 
[GCC 4.9.2 20150212 (Red Hat 4.9.2-6)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Готово ко вскрытию

Итак, с чего бы нам начать свой путь от полной несознанки до почти коре-девелопера? Взгляните на сорцы, которые вы только что скачали - дюжина директорий с неведомым содержимым. Совершенно не очевидно, что из этого является ядром системы и то, как отдельные компоненты связаны друг с другом. Думаю, что в таких условиях наиболее естественной была бы попытка пройти вместе с интерпретатором весь путь от начальной инициализации до интерпретации питоновской программы, что дало бы минимальное представление о сути проблемы.
Давайте использовать GDB - GNU Debugger. Не пугайтесь, нам понадобится лишь самый минимум его возможностей, так что ни один изящный питонист не пострадает, помимо этого мы частенько будем подглядывать в исходники - вы можете делать это прямо из отладчика.
Запускать мы будем вот такой питоновский код:
import os, signal
os.kill(os.getpid(), signal.SIGTRAP)
его мы заботливо положим в файл probe0.py где нибудь в файловой системе.
Что это и почему оно такое? Перед тем как начать выполнять какой либо скрипт интерпретатор долго готовится, в частности, грузит всякое-разное из стандартной библиотеки (помните непустой sys.modules в только что запущеной REPL сессии, например?), поэтому чтобы поймать в отладчике выполнение именно нашего скрипта мы пошлем себе SIGTRAP, который и будет перехвачен GDBой - вот такой нехитрый трюк.

gdb --args path/to/our/python path/to/probe0.py
GNU gdb (GDB) Fedora 7.8.2-38.fc21
Copyright (C) 2014 Free Software Foundation, Inc.
# blah blah
(gdb) run
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffff7127c07 in kill () from /lib64/libc.so.6
(gdb)

Мы имеем интерпретатор, остановленный на строке с лямбдой. Чтобы посмотреть нативный callstack, достаточно вызвать команду backtrace (или ее короткую версию bt).

#0-6 are skipped...
#7  PyEval_EvalFrameEx (f=0xa0c578, ...) -------- at Python/ceval.c:2835
#8  PyEval_EvalCodeEx (_co=0x7ffff70c7340, ...) - at Python/ceval.c:3586
#9  PyEval_EvalCode (co=0x7ffff70c7340, ...) ---- at Python/ceval.c:773
#10 run_mod (mod=0xa46768, ...) ----------------- at Python/pythonrun.c:2180
#11 PyRun_FileExFlags (fp=0x9ed880, ...) -------- at Python/pythonrun.c:2133
#12 PyRun_SimpleFileExFlags (fp=0x9ed880, ...) -- at Python/pythonrun.c:1606
#13 PyRun_AnyFileExFlags (fp=0x9ed880, ...) ----- at Python/pythonrun.c:1292
#14 run_file (fp=0x9ed880, ...) ----------------- at Modules/main.c:319
#15 Py_Main (argc=2, ...) ----------------------- at Modules/main.c:751
#16 main (argc=2, ...) -------------------------- at Modules/python.c:69

Первый столбик - это номера activation frame`ов сишных функций интерпретатора, лежат они в стековой памяти процесса в виде непрерывной простыни. Очевидно, что в самом начале вызывается функция main - шестнадцатый фрейм. Она выполняет самые первичные проверки - хватает ли памяти на запуск, удается ли декодировать аргументы командной строки и пр. Дальше, убедившись, что всё в порядке, управление передается функции Py_Main. Пока мы не сможем оценить её роли, но именно там происходит вызов Py_Initialize, который создает новый инстанс интерпретатора и выполняет его первоначальную настройку. Об этом мы обязательно поговорим как нибудь в другой раз, сейчас же нас интересуют куда более общие вопросы. Далее, вплоть до десятого фрейма включительно, система пытается собрать инстанс PyCodeObject. Тут мы тоже не будем пока вдаваться в подробности, но думаю ясно, что в этом процессе участвует компилятор, на вход которому подается длинная строка из .py файла, командной строки с ключем -с или, например, пользовательского ввода из REPLа. Так или иначе должен получиться "код", способный управлять интерпретатором точно так же, как машинные инструкции управляют аппаратным процессором. Функция PyEval_EvalCode есть тончайшая обертка вокруг PyEval_EvalCodeEx (восьмой фрейм), которая как раз и решает, что делать с тем кодом, который собрался из нашего питоновского исходника.


Что мы поняли на текущий момент

Мы с вами достигли первой важной точки на пути понимания CPython. Во-первых, мы поняли, что в CPython существует явным образом выделенная виртуальная машина, представленная функцией PyEval_EvalCodeEx (как мы увидим далее, это скорее PyEval_EvalFrameEx, но пока сойдет) и, во-вторых, эта машина на вход принимает специально собранный "код", представленный на низком уровне типом PyCodeObject.

Виртуальная машина

В сердце CPython лежит довольно простая стековая машина, управляемая байткодом, являющимся частью code objecta. Пойдем по порядку - сначала вызывается функция:
PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals, ...)
{
    PyCodeObject* co = (PyCodeObject*)_co;
    PyFrameObject *f;
    f = PyFrame_New(tstate, co, globals, locals);
    /* usualy it evals frame */
    return PyEval_EvalFrameEx(f,0);
       
}
Фактически, все что она делает - это создает новый питоновский фрейм f (о том, что это такое мы поговорим чуть ниже) и связывает с ним объект кода _co, который нужно будет исполнить. Непосредственно в этот момент ваша программа еще не исполняется - дальше "обычно" управление передается функции PyEval_EvalFrameEx, умеющей запускать фреймы (да, в питоне фреймы запускаются - "евалятся"). Я говорю "обычно", потому что в таком примере:
def some_foo():
    yield
some_foo()
компилятор увидев в теле функции some_foo слово yield, пометит код, связанный с этой функцией, специальным флагом. Он будет опознан внутри PyEval_EvalCodeEx, в результате чего some_foo() не исполнит тело функции (не запустит фрейм), вместо этого создастся новый generator object. Так вот, дальше "обычно" происходит следующее:
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    co = f->f_code;
    for (;;) {
        switch (opcode) {
            TARGET(LOAD_FAST)  {\* implementation of LOAD_FAST  *\}
            TARGET(LOAD_CONST) {\* implementation of LOAD_CONST *\}
            TARGET(STORE_FAST) {\* implementation of STORE_FAST *\}
            TARGET(POP_TOP)    {\* implementation of POP_TOP    *\}
            ...
    }
}
На вход подается указатель на питоновский фрейм. Фрейм - это внутреннее представление исполняющегося кода, такой контейнер с кодом и метаданными внутри. Именно фрейм хранит информацию о том, какой опкод сейчас исполняется и куда передать управление, когда код отработает. Последнее достигается хранением в атрибуте f_back ссылки на родительский фрейм, так что множество фреймов образует односвязный список. На один и тот же объект кода могу ссылаться множество фреймов, так что объект кода - это что-то вроде шаблона. В свете вышесказанного можно заключить, что питоновский фрейм очень похож на нативный фрейм, который создала бы программа на Си, внутри которой происходит вызов подпрограммы. Это почти правда, важно понимать, что питоновские фреймы - это обычные объекты, у них есть свой конструктор, память под них выделяется на куче, а не из стековой памяти, как для нативных фреймов, а также они подвержены сборке мусора. Подобно другим объектам, фреймы исполнения имеют представление внутри питоновского кода, что вкупе с другими возможностями делает питон языком с очень богатой интроспекцией.
Вернемся к PyEval_EvalFrameEx, на самом деле эта функция очень длинная, так что я немного укоротил ее код. Посмотрите насколько он простой (макросы вроде TARGET не должны вас смущать, они тоже простые) - из фрейма забирается ссылка на объект кода, а из него байтовая строка, изображающая опкоды. Виртуальная машина перебирая эти опкоды один за другим выполняет примитивные атомарные операции - питонячая программа выполняется. Да, вот так просто, никакой магии.


Объект кода

Инстанс питоновского кода возникает всякий раз, когда компилятор видит независимую единицу исполнения - модуль, функцию (метод) или класс. Помимо этого код можно собрать самостоятельно из произвольного набора выражений с помощью встроенной функции compile, например так
>>> code = compile('print(a + foo.bar())', '<string>', 'exec')
Обратите внимание, что переменные нигде не определены, при этом компилятор считает этот код совершенно валидным.
Мы можем запустить его, предоставив контекст:

>>> class Mock:
...     pass
... 
... Mock.bar = lambda: 'world!'
... eval(code, {'a':'Hello', 'foo': Mock})
Helloworld!
Вы, должно быть, знаете, что в стандартной библиотеке есть модуль dis, позволяющий дизассемблировать объекты питоновского кода:
>>> dis.disassemble(code)
  1           0 LOAD_NAME                0 (print)
              3 LOAD_NAME                1 (a)
              6 LOAD_NAME                2 (foo)
              9 LOAD_ATTR                3 (bar)
             12 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             15 BINARY_ADD
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 POP_TOP
             20 LOAD_CONST               0 (None)
             23 RETURN_VALUE
Что мы видим? На стек нашей стековой машины последовательно грузятся сущности под именами print, a, foo и так далее. Откуда происходит эта загрузка? Точно не из байткода, потому что на момент компиляции корректность таких ссылок никто не гарантировал. Значит существует контекст, объект кода знает о его интерфейсе (он грузит из него сущности, зная их имена), но не знает о реализации, т.е. что там лежит. Это полезно в случае рекурсии, когда один и тот же код переиспользуется многократно внутри разных контекстов.
Детали реализации PyCodeObject вы можете посмотреть в исходниках (Include/code.h). О коде нам важно уяснить вот что:
1. Он включает в себя байтовую строку - байткод, который непосредственно рулит виртуальной машиной.
2. Этот байткод запускается внутри определенного контекста, для обращения к которому ему так же нужен интерфейс.
3. Байткод и интерфейс собраны в один объект - объект кода.
4. Контекст является внешней по отношению к коду сущностью.
5. Фрейм исполнения содержит в себе ссылку на объект кода и на контекст, когда исполняется ваш питоновский скрипт, на низком уровне запущена PyEval_EvalFrameEx.


Думаю на сегодня хватит

Разумеется, непосредственную работу выполняют внутренности объектов (даже, точнее, их классов), так что когда мы пишем:
1 + 'hello'
то именно класс int должен решить, может ли он обработать такое суммирование или нет, в то время как функция PyEval_EvalFrameEx лишь инициирует это суммирование. Поэтому, познакомившись сегодня с фронтендом, мы узнали одновременно и многое и совсем ничего, но не отчаивайтесь, это всего лишь введение, со временем мы обозреем все важные вопросы.
Спасибо за ваше время, надеюсь, что эта тема была интересной.

суббота, апреля 04, 2015

Немного метамагии, или как работает class statement.

Говоря о метапрограммировании в python чаще всего подразумевают использование декораторов или метаклассов, иногда сюда приплетают импорт хуки и даже генерацию питоновского кода с последующим execом его в целевом неймспейсе (классе, например). Все это - вполне себе production ready приемы, так, например, реализацию питоновских ORM движков сложно себе представить без метаклассов и дескрипторов. Такие библиотеки как pytest вообще невозбранно хачат весь ваш код, что особенно радует разработчиков плагинов, когда дело доходит до дебага.


Собирай это!

Давайте спустимся с небес на землю. Скажите, знаете ли вы, что делает компилятор, когда видит class statement? Для простоты дизассемблируем объявление пустого класса:
>>> import dis
... dis.dis('class MyClass: pass')
  1           0 LOAD_BUILD_CLASS
              1 LOAD_CONST               0 (<code object MyClass at ...>)
              4 LOAD_CONST               1 ('MyClass')
              7 MAKE_FUNCTION            0
             10 LOAD_CONST               1 ('MyClass')
             13 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             16 STORE_NAME               0 (MyClass)
             19 LOAD_CONST               2 (None)
             22 RETURN_VALUE
Первым выполняется некий не имеющий аргументов опкод LOAD_BUILD_CLASS. Затем создается функция с именем MyClass и кодом <code object MyClass at ...>. Дальше небольшая тонкость: опкод со сдвигом тринадцать (вызов функции) не может быть связан с функцией MyClass, ведь ей доступен только один аргумент – строчка 'MyClass', в то время как дизассемблер утверждает, что вызов произойдет с двумя аргументами. Так что единственный вариант, который мы можем предположить – это то, что LOAD_BUILD_CLASS грузит на стек некую специальную функцию, которая принимает в качестве первого аргумента функцию, идейно эквивалентную телу класса и строку – название класса (и, возможно, что то еще). Результат вызова этой функции и есть готовый класс, который мы заботливо кладем в переменную MyClass (опкод со сдвигом 16). Все здорово, но что это за таинственная функция, создающая классы? Это не может быть type, не та сигнатура.
Разрешить эту загадку в общем-то довольно легко, достаточно заглянуть в исходники интерпретатора. В файле Python/ceval.c лежит функция PyEval_EvalFrameEx – сердце рантайма питона. Именно тут происходит выполнение тех инструкций, которые сгенерились из вашего питоновского кода в процессе компиляции. Я немного подкоротил код обработчика LOAD_BUILD_CLASS, он выглядит так:
TARGET(LOAD_BUILD_CLASS) {
    PyObject *bc;
    bc = _PyDict_GetItemId(f->f_builtins, &PyId___build_class__);
    PUSH(bc);
    DISPATCH();
}
Или, говоря по-русски – возьми из builtins функцию __build_class__ и положи на value stack. Скажите честно, кто из вас знал о том, что в билтинах есть такая функция? Я так и думал :-)
Ок, теперь зная куда копать, мы можем вдоволь повеселиться. Давайте ее немного перегрузим, ну например так
>>> __builtins__['__build_class__'] = lambda *a, **kwa: print(a, kwa)
... class MyClass(int, str, metaclass=type, foo='bar'):
...     def __init__(self):
...         pass
(<function MyClass at ...>, 'MyClass', <class 'int'>, <class 'str'>)
{'metaclass': <class 'type'>, 'foo': 'bar'}
Таким образом справедлива такая распаковка:
func_object, class_name, *bases = args    (1)
class_kwargs = kwargs                     (2)
где class_kwargs – это все именованные аргументы в объявлении класса, значит даже metaclass – не более чем очередной kwarg и мы вправе обработать его как нам угодно.

Use cases.

Для дальнейших манипуляций нам понадобится простенькая инфраструктура, я подготовил такую тут. В простейшем виде наш тестовый cтенд будет выглядеть так:
def callback(builder, *args, **kwargs):
    return builder(*args, **kwargs)

with wonderland(callback):
    class MyClass:
        pass
Внутри блока with будем объявлять классы, задача по их сборке будет делегирована функции callback, возвращать она должна уже готовый класс. Аргумент builder – это оригинальный __build_class__, *args и **kwargs – это все, что этому билдеру было бы передано в ванильном случае, так что builder(*args, **kwargs) – это и есть класс, собранный обычным образом.
Перед нами открылся невиданный простор для экспериментов. Давайте, например, посмотрим на func_object из (1).Функция эта сама по себе никому не нужна, она лишь служит оберткой для кода. Что за код такой? Заведем callback:
def callback(builder, *args, **kwargs):
    func, *_ = args
    print(func.__code__.co_consts)
    return builder(*args, **kwargs)
Добавьте в MyClass пару методов и запустите. Оказывается, что методы, объявленные в классе, попадают в func.__code__.co_consts, причем сами они хранятся там в виде соответсвующих объектов кода. К нашему сожалению CodeType иммутабелен, так что кастомизировать класс путем изменения его кода мы не можем (ну ладно, можем, но это совсем неудобно). Максимум что можно сделать на этом этапе – это изменить список его базовых классов, имя, полностью пересадить у func ее код, воспользовавшись конструктором из types, либо навесить на класс декоратор. Также можно играться с уже готовым классом, просто сохранив выхлоп builder(*args, **kwargs) в локальную переменную.
Следующий шаг, сильно упрощающий нам жизнь – это прописывание классу нашего кастомного метакласса, мы ведь помним, что именованный аргумент metaclass ничем не выделяется из собратьев. Сделать это очень просто:
class MyMeta(type):
    def __new__(cls, name, bases, attrs):
        print('hello from metaclass %s' % cls.__name__)
        return super().__new__(cls, name, bases, attrs)

def callback(builder, *args, **kwargs):
    kwargs['metaclass'] = MyMeta
    return builder(*args, **kwargs)
Вот, это уже действительно что то. Думаю объяснять, что может нам дать свой, написанный с определенной целью, метакласс не нужно (или нужно? марш к Бизли!).
Тут есть как минимум две проблемы:
1. пусть в kwargs был-таки непустой метакласс, если просто заменить его своим метаклассом то мы сломаем логику приложения, так что придется динамически строить третий метакласс, сочетающий возможности обоих.
2. пусть kwargs даже пуст, метакласс отличный от type может быть у одного из родителей класса, что приведет к metaclass conflict. Это тоже лечится, но как-то фу.
Это грустно и я ничего хорошего посоветовать не могу. Механизм, который смог бы рулить всем этим добром в общем виде представляется мне довольно сложным.
К чему я это все? А к тому, что это еще один подход к метапрограммированию на питоне, ориентированный на классы. Его огромный плюс – код находящийся в блоке with не знает о том, что с ним будут что-то делать, значит это может быть чей-то чужой код. Ну и понятно, что внутри with классы можно просто импортировать из внешнего модуля, так что инструментировать код того же Django или другой большой библиотеки можно не поменяв в ней ни строчки кода.
Надеюсь, что это было кому-то интересно, теперь можете смело выпендриваться перед коллегами. Спасибо за ваше время.