вторник, 14 декабря 2010 г.

Daily Shit: Внутренности пайтона. Класс abc.

В который раз столкнулся со внутренностями CPython и получил фейспальму.
Дело в том, что для фреймворка нам необходимо использовать интерфейсы. Сразу было понятно что abc нам не хватит, поэтому надо было что-то бОльшее. Был рассмотрен zope.interface, но не устроило то, что для имплементирывания класса нужно вызывать функцию implements в теле класса, что не очень красиво. Увы, такие вещи для многих становятся критичными во время выбора инструмента и могут послужить минусом для фреймворка в целом.



В общем, решили что нужно просто добавить пару декораторов к abc и несколько других умных фич.
Первой фишкой, которую хотелось имплементировать, оказалась "фишка" вывода докстринга функции, которая не имплементирована, т.е. если кто-то не имплементировал фукцнию, то при запуске приложения выходит ошибка с докстрингом.

Казалось бы, всё просто. Однако, эксепшн мало о чём говорит. Забавно, но эксепш даже не специального типа, а просто TypeError. Чтобы проанализировать что и как, нужно было бы посмотреть сырец модуля abc. Однако, достаточно просто грепнуть abc.py по raise чтобы понять что abc.py это не всё, а сама ошибка берётся откуда-то ещё. Оказалось что всё сложнее, что корни abc лежат в сырцах CPython.
Греп по сырцам CPython сразу дал ответ — typeobject.c. В общем, вот что нам интересно:


static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
int err = 0;
if (excess_args(args, kwds)) {
if (type->tp_new != object_new &&
type->tp_init != object_init)
{
err = PyErr_WarnEx(PyExc_DeprecationWarning,
"object.__new__() takes no parameters",
1);
}
else if (type->tp_new != object_new ||
type->tp_init == object_init)
{
PyErr_SetString(PyExc_TypeError,
"object.__new__() takes no parameters");
err = -1;
}
}
if (err < 0)
return NULL;

if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
static PyObject *comma = NULL;
PyObject *abstract_methods = NULL;
PyObject *builtins;
PyObject *sorted;
PyObject *sorted_methods = NULL;
PyObject *joined = NULL;
const char *joined_str;

/* Compute ", ".join(sorted(type.__abstractmethods__))
into joined. */
abstract_methods = type_abstractmethods(type, NULL);
if (abstract_methods == NULL)
goto error;
builtins = PyEval_GetBuiltins();
if (builtins == NULL)
goto error;
sorted = PyDict_GetItemString(builtins, "sorted");
if (sorted == NULL)
goto error;
sorted_methods = PyObject_CallFunctionObjArgs(sorted,
abstract_methods,
NULL);
if (sorted_methods == NULL)
goto error;
if (comma == NULL) {
comma = PyString_InternFromString(", ");
if (comma == NULL)
goto error;
}
joined = PyObject_CallMethod(comma, "join",
"O", sorted_methods);
if (joined == NULL)
goto error;
joined_str = PyString_AsString(joined);
if (joined_str == NULL)
goto error;

PyErr_Format(PyExc_TypeError,
"Can't instantiate abstract class %s "
"with abstract methods %s",
type->tp_name,
joined_str);
error:
Py_XDECREF(joined);
Py_XDECREF(sorted_methods);
Py_XDECREF(abstract_methods);
return NULL;
}
return type->tp_alloc(type, 0);
}

И сами методы

static PyObject *
type_abstractmethods(PyTypeObject *type, void *context)
{
PyObject *mod = NULL;
/* type itself has an __abstractmethods__ descriptor (this). Don't return
that. */
if (type != &PyType_Type)
mod = PyDict_GetItemString(type->tp_dict, "__abstractmethods__");
if (!mod) {
PyErr_Format(PyExc_AttributeError, "__abstractmethods__");
return NULL;
}
Py_XINCREF(mod);
return mod;
}

static int
type_set_abstractmethods(PyTypeObject *type, PyObject *value, void *context)
{
/* __abstractmethods__ should only be set once on a type, in
abc.ABCMeta.__new__, so this function doesn't do anything
special to update subclasses.
*/
int res = PyDict_SetItemString(type->tp_dict,
"__abstractmethods__", value);
if (res == 0) {
PyType_Modified(type);
if (value && PyObject_IsTrue(value)) {
type->tp_flags |= Py_TPFLAGS_IS_ABSTRACT;
}
else {
type->tp_flags &= ~Py_TPFLAGS_IS_ABSTRACT;
}
}
return res;
}


Как видно из сырцов, abc это не что-то внешнее, это часть пайтона. Вся реализация идёт через установку объекту флага Py_TPFLAGS_IS_ABSTRACT.
Флаг ставится только при вызове сеттера __abstractmethods__ и если value является сетом:


int res = PyDict_SetItemString(type->tp_dict, "__abstractmethods__", value);


Самое интересное же оказалось в методе __new__, где всё делается просто как 2 копейки:

PyErr_Format(PyExc_TypeError,
"Can't instantiate abstract class %s "
"with abstract methods %s",
type->tp_name,
joined_str);


Собственно, суть в том, что нужно сразу после создания конечного класса(который имплементирует функции) объявить список абстрактных функций через присваивание списка полю __abstractmethods__. Не сделали — ничего и не будет.
А теперь о том, как работает abc сейчас:
1) Создаётся интерфейс с метаклассом ABCMeta. В момент его создания, вызывается метод __new__ мета-класса, который обрабатывает содержимое класса и инициализирует __abstractmethods__. Тадам! Поставили флаг что данный класс — абстрактрый.
2) Создаётся имплементация интерфейса через наследование интерфейса. Повторно вызывается метод __new__ метакласса интерфейса! Собственно, тогда и делаются все нужные чудеса.
3) В CPython райсится эксепшн.
Собственно, что осталось сделать — отловить эксепшн в __call__, определить какой метод не имплементирован и вывести его докстринг.

Такие дела. Вот только то, как оно внутри всё устроено, мне очень не понравилось. Одно хорошо — отделались не такой большой кровью.

Результаты трудов можно наблюдать среди "адаптаций" в agatsuma - здесь.
Если вы хотите чтобы наша версия обёртки для абстрактных классов была включена в agatsuma.commons, то можете отписать об этом в багтрекер, ибо только моего голоса не хватит чтобы образумить второго разработчика агатсумы.

P.S.: Это старый документ, запощеный в juick, может где-то что-то не так.

Комментариев нет:

Отправить комментарий