【问题标题】:Python extension in C - MetaclassC 中的 Python 扩展 - 元类
【发布时间】:2019-03-28 04:08:07
【问题描述】:

我有以下 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实际上叫DefaultConfigChild实际上是从DefaultConfig派生的用户定义类。

我正在将MetaParent 翻译成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;
}

这是用于测试模块custom的python代码:

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;
}

【问题讨论】:

    标签: python c inheritance metaclass python-extensions


    【解决方案1】:

    元类只不过是一种类型,它被用作类(类型)的类型 (ob_type!)。 ..(很清楚,不是吗)...ParentType 不继承自 MetaType,而是 `MetaType 的实例。

    因此,&amp;MetaType 应该 去的地方是ParentType.ob_type

    PyModule_AddObject(module, "Meta", (PyObject *) &MetaType);
    
    ParentType.ob_type = &MetaType;
    
    if (PyType_Ready(&ParentType) < 0)
    

    PyType_Ready 检查ob_type 字段——如果是NULL,则取.tp_baseob_type;但如果 ob_type 已经设置,则保持原样。

    其实你可以在ParentType初始化器中设置:

    PyVarObject_HEAD_INIT(&MetaType, 0)
    

    第一个参数转到ob_type 字段。

    【讨论】:

    • 似乎python源代码有一个名为PyId_metaclass的特殊属性,它用于确定元类......github.com/python/cpython/blob/…
    • @JoshWeinstein 相反,这只是 字符串 "metaclass",用于从 kwargs 获取 metaclass=FooBar
    • 设置ob_type 是不够的,因为问题中的代码还有其他问题。例如,MetaObject 不包含PyTypeObject 基成员,而MetaTypetp_base 需要设置为PyType_Type
    • 另外,像 PyId_metaclass 之类的东西不仅仅是字符串——它们是用于管理内部静态字符串的包装器。不过,Python 最终仍然使用它通过字符串名称从 kwargs 中获取 metaclass。它不是任何特殊属性,也不是实际类对象的元类的存储方式。
    • @Nelson:乍一看,您不需要MetaObject 中的PyObject_HEADPyTypeObjectPyObject_HEAD 负责处理。可能还有其他问题。请参阅 C API 教程的 relevant sectionModules/xxsubtype.c 以获取在 C 中实现内置类型的子类的示例。
    【解决方案2】:

    没有直接的方法可以做到这一点。根据py docs,没有成员或标志可以直接表明一个类是另一个类的元类。负责指示元类的属性是inside the class dictionary。您可以实现一些修改 .tp_dict 成员的东西,但如果通过字典 C-API 完成,这实际上被视为 unsafe

    警告在字典 C-API 上使用 PyDict_SetItem() 或以其他方式修改 tp_dict 是不安全的。

    编辑:

    python source code 看来,元类似乎是通过 C 字典 API 作为 id 访问的,但这样做的方法以 _ 为前缀,并且不会出现在任何文档中。

        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;
            }
    

    这些方法是"limited 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 */
    

    【讨论】:

    • 有一个成员表示一个类的元类;它是 PyObject_HEAD 的一部分,它与用于任何其他对象类的成员相同。 __metaclass__ dict 条目未在 Python 3 中使用,并且仅在创建类期间在 Python 2 中使用过,而不是用于确定已创建类的元类。
    • 在现有类的字典中设置__metaclass__ 不会影响其元类。此外,在调用PyType_Ready 之后与tp_dict 混淆仍然是不安全的。
    • 你能链接到 PyObject_HEAD 的那个部分在哪里吗?此处未记录docs.python.org/3/c-api/structures.html#c.PyObject
    • /usr/include/python3.6/object.h:83 #define PyObject_HEAD PyObject ob_base;。我不明白 PyObject 如何包含有关类型元类的信息
    • 更新了我的答案以在 python 源代码中包含检查对象元类的位置。在PyObject_HEAD不是
    猜你喜欢
    • 2021-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多