Расширение Python в C — метакласс

У меня есть следующий код Python:

class Meta(type):
    def __call__(cls, *args, **kwargs):
        obj = type.__call__(cls, *args, **kwargs)
        # Only do checks for subclasses
        if cls.__name__ == 'Parent':
            return obj
        required_attrs = ['x']
        for ra in required_attrs:
            if ra not in dir(obj):
                fmt = 'Subclasses of Parent must define the %s attribute'
                raise NotImplementedError(fmt % ra)
        return obj

class Parent(metaclass=Meta):
    pass

class Child(Parent):
    def __init__(self):
        self.x = True

Meta используется только для того, чтобы потребовать, чтобы Child определял определенные атрибуты. Эта структура классов должна оставаться как есть, потому что так устроен мой проект. Parent на самом деле называется DefaultConfig, а Child на самом деле является определяемым пользователем классом, производным от DefaultConfig.

Я работаю над переводом Meta и Parent в расширение C. Это модуль:

#include <Python.h>
#include <structmember.h>

#define ARRLEN(x) sizeof(x)/sizeof(x[0])


typedef struct {
    PyObject_HEAD
} MetaObject;

typedef struct {
    PyObject_HEAD
} ParentObject;


static PyObject *Meta_call(MetaObject *type, PyObject *args, PyObject *kwargs) {
    PyObject *obj = PyType_GenericNew((PyTypeObject *) type, args, kwargs);

    // Only do checks for subclasses of Parent
    if (strcmp(obj->ob_type->tp_name, "Parent") == 0)
        return obj;

    // Get obj's attributes
    PyObject *obj_dir = PyObject_Dir(obj);
    if (obj_dir == NULL)
        return NULL;

    char *required_attrs[] = {"x"};

    // Raise an exception of obj doesn't define all required_attrs
    PyObject *attr_obj;
    int has_attr;
    for (int i=0; i<ARRLEN(required_attrs); i++) {
        attr_obj = PyUnicode_FromString(required_attrs[i]);
        has_attr = PySequence_Contains(obj_dir, attr_obj);
        if (has_attr == 0) {
            printf("Subclasses of Parent must define %s\n", required_attrs[i]);
            // raise NotImplementedError
            return NULL;
        } else if (has_attr == -1) {
            return NULL;
        }
    }

    return obj;
}


static PyTypeObject MetaType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Meta",
    .tp_basicsize = sizeof(MetaObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = PyType_GenericNew,
    .tp_call = (ternaryfunc) Meta_call,
};

static PyTypeObject ParentType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Parent",
    .tp_basicsize = sizeof(ParentObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = PyType_GenericNew,
};


static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_size = -1,
};


PyMODINIT_FUNC PyInit_custom(void) {
    PyObject *module = PyModule_Create(&custommodule);
    if (module == NULL)
        return NULL;

    // Should Parent inherit from Meta?
    ParentType.tp_base = &MetaType;

    if (PyType_Ready(&MetaType) < 0)
        return NULL;
    Py_INCREF(&MetaType);
    PyModule_AddObject(module, "Meta", (PyObject *) &MetaType);

    if (PyType_Ready(&ParentType) < 0)
        return NULL;
    Py_INCREF(&ParentType);
    PyModule_AddObject(module, "Parent", (PyObject *) &ParentType);

    return module;
}

Это код Python, используемый для тестирования модуля custom:

import custom

class Child(custom.Parent):
    def __init__(self):
        self.x = True

if __name__ == '__main__':
    c = Child()

К сожалению, в структуре PyTypeObject нет члена .tp_meta, так как мне указать Meta в качестве метакласса Parent?


ИЗМЕНИТЬ:

Модифицированный код C:

#include <Python.h>
#include <structmember.h>

#define ARRLEN(x) sizeof(x)/sizeof(x[0])


typedef struct {
    PyObject_HEAD
    PyTypeObject base;
} MetaObject;

typedef struct {
    PyObject_HEAD
} ParentObject;


static PyObject *Meta_call(MetaObject *type, PyObject *args, PyObject *kwargs) {
    PyObject *obj = PyType_GenericNew((PyTypeObject *) type, args, kwargs);

    // Only do checks for subclasses of Parent
    if (strcmp(obj->ob_type->tp_name, "Parent") == 0)
        return obj;

    // Get obj's attributes
    PyObject *obj_dir = PyObject_Dir(obj);
    if (obj_dir == NULL)
        return NULL;

    char *required_attrs[] = {"x"};

    // Raise an exception of obj doesn't define all required_attrs
    PyObject *attr_obj;
    int has_attr;
    for (int i=0; i<ARRLEN(required_attrs); i++) {
        attr_obj = PyUnicode_FromString(required_attrs[i]);
        has_attr = PySequence_Contains(obj_dir, attr_obj);
        if (has_attr == 0) {
            printf("Subclasses of Parent must define %s\n", required_attrs[i]);
            // raise NotImplementedError
            return NULL;
        } else if (has_attr == -1) {
            return NULL;
        }
    }

    return obj;
}


static PyTypeObject MetaType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Meta",
    .tp_basicsize = sizeof(MetaObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = PyType_GenericNew,
    .tp_call = (ternaryfunc) Meta_call,
};

static PyTypeObject ParentType = {
    PyVarObject_HEAD_INIT(&MetaType, 0)
    .tp_name = "custom.Parent",
    .tp_basicsize = sizeof(ParentObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = PyType_GenericNew,
};


static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_size = -1,
};


PyMODINIT_FUNC PyInit_custom(void) {
    PyObject *module = PyModule_Create(&custommodule);
    if (module == NULL)
        return NULL;

    MetaType.tp_base = &PyType_Type;
    if (PyType_Ready(&MetaType) < 0)
        return NULL;
    Py_INCREF(&MetaType);
    PyModule_AddObject(module, "Meta", (PyObject *) &MetaType);

    if (PyType_Ready(&ParentType) < 0)
        return NULL;
    Py_INCREF(&ParentType);
    PyModule_AddObject(module, "Parent", (PyObject *) &ParentType);

    return module;
}

person Nelson    schedule 23.10.2018    source источник


Ответы (2)


Метакласс — это не что иное, как тип, который используется как тип (ob_type!) класса (типа)... (очистить, не так ли)... ParentType не наследуется от MetaType, а является экземпляром `MetaType.

Следовательно, место, куда &MetaType должно идти, если оно работает должным образом, это ParentType.ob_type:

PyModule_AddObject(module, "Meta", (PyObject *) &MetaType);

ParentType.ob_type = &MetaType;

if (PyType_Ready(&ParentType) < 0)

PyType_Ready проверяет поле ob_type - если оно равно NULL, оно берет ob_type из .tp_base; но если ob_type уже установлен, он остается как есть.

На самом деле вы можете установить его в инициализаторе ParentType:

PyVarObject_HEAD_INIT(&MetaType, 0)

Первый аргумент идет в поле ob_type.

person Antti Haapala    schedule 23.10.2018
comment
Похоже, что в исходном коде Python есть специальный атрибут PyId_metaclass, который используется для определения метакласса. c#L151" rel="nofollow noreferrer">github.com/python/cpython/blob/ - person Josh Weinstein; 24.10.2018
comment
@JoshWeinstein, напротив, это всего лишь строка "metaclass", используемая для получения metaclass=FooBar от kwargs. - person Antti Haapala; 24.10.2018
comment
Однако установки ob_type будет недостаточно, потому что есть другие проблемы с кодом в вопросе. Например, MetaObject не включает базовый элемент PyTypeObject, а tp_base для MetaType необходимо установить на PyType_Type. - person user2357112 supports Monica; 24.10.2018
comment
Кроме того, такие вещи, как PyId_metaclass, не являются совсем просто строками — это оболочки, используемые для управления интернированными статическими строками. Однако Python по-прежнему использует его для получения metaclass из kwargs по имени строки. Это не какой-то особый атрибут и не то, как хранится метакласс фактического объекта класса. - person user2357112 supports Monica; 24.10.2018
comment
Изменение структуры ParentType для использования PyVarObject_HEAD_INIT(&MetaType, 0) (после исправления ошибок, упомянутых пользователем 2357112) вызывает segfault. Смотрите обновленный код - person Nelson; 24.10.2018
comment
@Nelson: На первый взгляд, вам не нужен PyObject_HEAD в MetaObject; PyObject_HEAD из PyTypeObject позаботится об этом. Могут быть другие проблемы. См. соответствующий раздел руководства по C API, и Modules/xxsubtype.c для примера реализации подклассов встроенных -in типы в C. - person user2357112 supports Monica; 24.10.2018

Прямого способа сделать это нет. Согласно документам py, нет членов или флагов для прямого указать, что класс является метаклассом другого. Атрибут, отвечающий за указание метакласса, находится внутри словаря класса< /а>. Вы можете реализовать что-то, что изменяет элемент .tp_dict, но на самом деле это считается небезопасно, если это делается через словарь C-API.

Предупреждение Небезопасно использовать PyDict_SetItem() или иным образом изменять tp_dict с помощью C-API словаря.

РЕДАКТИРОВАТЬ:

Из исходный код Python кажется метаклассом доступен как id через API словаря C, но методы для этого имеют префикс _ и не появляются ни в одной документации.

    meta = _PyDict_GetItemId(mkw, &PyId_metaclass);
    if (meta != NULL) {
        Py_INCREF(meta);
        if (_PyDict_DelItemId(mkw, &PyId_metaclass) < 0) {
            Py_DECREF(meta);
            Py_DECREF(mkw);
            Py_DECREF(bases);
            return NULL;
        }

Эти методы являются частью "ограниченного API" и могут быть используется путем определения макроса Py_LIMITED_API

PyAPI_FUNC(PyObject *) _PyDict_GetItemId(PyObject *dp, struct _Py_Identifier *key);
#endif /* !Py_LIMITED_API */
PyAPI_FUNC(int) PyDict_SetItemString(PyObject *dp, const char *key, PyObject *item);
#ifndef Py_LIMITED_API
PyAPI_FUNC(int) _PyDict_SetItemId(PyObject *dp, struct _Py_Identifier *key, PyObject *item);
#endif /* !Py_LIMITED_API */
person Josh Weinstein    schedule 23.10.2018
comment
Есть член, указывающий метакласс класса; это часть PyObject_HEAD, и это тот же член, который используется для любого другого класса объекта. Запись __metaclass__ dict не используется в Python 3, а в Python 2 она использовалась только во время создания класса, а не для определения метакласса уже созданного класса. - person user2357112 supports Monica; 23.10.2018
comment
Установка __metaclass__ в словаре существующего класса не повлияет на его метакласс. Кроме того, возиться с tp_dict после вызова PyType_Ready по-прежнему небезопасно. - person user2357112 supports Monica; 23.10.2018
comment
Можете ли вы указать, где находится эта часть PyObject_HEAD? здесь не задокументировано docs.python.org/3/c-api /structures.html#c.PyObject - person Josh Weinstein; 23.10.2018
comment
/usr/include/python3.6/object.h:83 #define PyObject_HEAD PyObject ob_base;. Я не понимаю, как PyObject содержит информацию о метаклассе типа - person Nelson; 23.10.2018
comment
Обновлен мой ответ, чтобы включить место в исходном коде Python, которое проверяет метакласс объектов. Его нет в PyObject_HEAD - person Josh Weinstein; 24.10.2018
comment
@Nelson: это будет struct _typeobject *ob_type; внутри определения PyObject , который записывает тип объекта. Метакласс класса — это его тип. - person user2357112 supports Monica; 24.10.2018
comment
Кроме того, определение Py_LIMITED_API не требуется для доступа к какой-либо части C API. Определение Py_LIMITED_API ограничивает вас подмножеством C API. - person user2357112 supports Monica; 24.10.2018