2

我正在努力理解一些围绕 PyCFunction_New 的PyCXX代码(C++ Python 包装器)。

有人能解释一下这个功能是如何工作的吗?

(我无法从 CPython源代码中弄清楚。)


在这里,我将详细说明我遇到的问题。我在上面划了一条线,因为这可能不会有那么普遍的用途。

问的原因是我正在处理奇怪的代码。我有一个关键字方法处理函数:

    static PyObject* keyword_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args, 
                                      PyObject* _keywords ) { }

它被存储为:

PyMethodDef meth_def_ext;
meth_def_ext.ml_meth = reinterpret_cast<PyCFunction>( _handler );
meth_def.ml_flags = METH_VARARGS | METH_KEYWORDS;

然后它被捆绑到 PyCFunction_New 中:

        MethodDefExt<T>* method_def_ext = ...;

        Tuple args{2}; // Tuple wraps a CPython Tuple
        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() );

        return Object(func, true);
    }

我是否正确地假设 CPython 会负责将其类型转换回一个 3 参数函数,其中第一个参数是 args(它与处理程序的 _self_and_name_tuple 第一个参数匹配)?

而 CPython 只能从它必须解析的事实中知道:'myFunc(7, a=1)' 它实际上正在处理一个关键字 aka 3-param 函数?

这看起来不对。

也许 CPython 正在将 args 1类型转换回 PyMethodDef,然后检查它的 .ml_flags

如果发生这种情况,那么我需要知道,因为我正在使用的代码只是:

template<class T>
class MethodDefExt //: public PyMethodDef <-- I commented this out
{
    // ... Constructors ...

    PyMethodDef               meth_def;

    method_noargs_function_t  ext_noargs_function  = nullptr;
    method_varargs_function_t ext_varargs_function = nullptr;
    method_keyword_function_t ext_keyword_function = nullptr;

    Object                    py_method;
};

在它的原始形式中,我认为它一定有两个 PyMethodDef 副本并且第一个从未被触及,因为它是基类

如果这真的发生了,即如果这个类确实通过 PyCFunction_New 的内部将类型转换回 PyMethodDef,那么这是不可靠的。

当然有人可以在 MethodDefExt 的前面添加一个成员变量,然后类型转换就会中断。这是脆弱的...


我正在处理的类允许未来的 C++ 编码器实现自定义 Python 类型,并在此类型中实现可以从 Python 调用的方法。

所以他们派生MyExt : CustomExt并编写方法:

// one of these three
MyExt::foo(){...} 
MyExt::foo(PyObject* args){...}
MyExt::foo(PyObject* args, PyObject* kw){...}

现在他们必须通过调用以下三个函数中的一个来将此方法存储在查找中:

    typedef Object (T::*method_noargs_function_t)();
    static void add_noargs_method( const char* name, 
                                   method_noargs_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                   {name,function,noargs_handler,doc};
    }

    typedef Object (T::*method_varargs_function_t)( const Tuple& args );
    static void add_varargs_method( const char* name, 
                                    method_varargs_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                    {name,function,varargs_handler,doc};
    }

    typedef Object (T::*method_keyword_function_t)( const Tuple& args, const Dict& kws );
    static void add_keyword_method( const char* name, 
                                    method_keyword_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                    {name,function,keyword_handler,doc};
    }

请注意,每个都有一个关联的处理函数。这些处理函数是CustomExt的静态方法——因为指向静态方法的指针可以从CPython 中调用,即它只是一个标准的C 风格函数指针。

所以当 Python 想要这个foo函数的指针时,我们在这里截取:

    // turn a name into function object
    virtual Object getattr_methods( const char* _name )
    {
        std::string name{ _name };

        // see if name exists and get entry with method
        auto i = lookup().find( name );

        DBG_LINE( "packaging relevant C++ method and extension object instance into PyCFunction" );

        // assume name was found in the method map
        MethodDefExt<T>* method_def_ext = i->second;

        // this must be the _self_and_name_tuple that gets received
        //   as the first parameter by the handler
        Tuple args{2};

        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

构造一个 Python 函数,该函数将调用此方法的处理程序(同时在此对象 args[0] 中传递方法本身 args 1的详细信息)。处理程序将在捕获错误时负责运行该方法。

请注意,此时我们不执行处理程序而是将此 Python 函数返回给 Python 运行时 也许 Python 编码器不希望执行该函数,而只是想获取指向它的指针: fp = MyExt.func;

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() );

X (见下文) & method_def_ext->meth_def 拉出处理函数,它是三个处理函数之一然而,由于 MethodDefExt 的构造函数,它们都被类型转换为 PyCFunction 对象,这意味着参数列表对于关键字处理函数是错误的。

        return Object(func, true);
    }

(我不得不打破注释,因为 SO 的格式化程序没有将它们作为代码注释处理)

我正在努力解决的是:假设foo是一个接受关键字的函数,所以它的签名将是:

MyExt::foo(PyObject* args, PyObject* kw)

匹配的处理程序如下所示:

    static PyObject* noargs_handler( PyObject* _self_and_name_tuple, 
                                     PyObject*  ) { }

    static PyObject* varargs_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args ) { }

    static PyObject* keyword_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args, 
                                      PyObject* _keywords ) { }

即第三个。我读过 Python 提供了一个额外的第一个_self_and_name_tuple参数。

当我们将 foo 注册到查找中时,我们提供了这个处理程序:

    typedef                               Object (T::*method_keyword_function_t)( const Tuple& args, const Dict& kws );
    static void add_keyword_method( const char* name, method_keyword_function_t function ) {
        methods()[std::string{name}] = new MethodDefExt<T> {name,function,keyword_handler,doc};
    }

并查看MethodDefExt的特定构造函数,

    // VARARGS + KEYWORD
    MethodDefExt (
        const char* _name,
        method_keyword_function_t _function,
        method_keyword_call_handler_t _handler
    )
    {
        meth_def.ml_name = const_cast<char *>( _name );
        meth_def.ml_doc  = nullptr;
        meth_def.ml_meth = reinterpret_cast<PyCFunction>( _handler );
        meth_def.ml_flags = METH_VARARGS | METH_KEYWORDS;

        ext_noargs_function = nullptr;
        ext_varargs_function = nullptr;
        ext_keyword_function = _function;
    }

...可以看出它将这个处理程序类型转换为PyCFunction

但是PyCFunction只需要两个参数!!!

typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);

我们将处理程序类型转换为此。这些处理程序有 2 或 3 个参数。

这看起来真的很不对劲。

然后返回,所以当 CPython 想要执行 foo 时,如上所述,它将获取这个meth_def.ml_meth并将其输入PyCFunction_New

        Tuple args{2};

        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() ); // https://github.com/python/cpython/blob/master/Objects/methodobject.c#L19-L48

所以我可以猜测一下: * PyCFunction_New 的第一个参数必须是 PyCFunction 函数指针 * 第二个参数必须是 PyObject* _self_and_name_tuple

我们将这个反馈给 CPython 我的猜测是,当 CPython 想要使用 'foo(7, a=1,b=2)' 时,它会将 7 打包成 args,a=1,b=2 打包成 kwds,然后调用:

[the PyCFunction function pointer](_self_and_name_tuple, args, kwds)
4

2 回答 2

4

我会冒险回答:

PyObject* PyCFunction_New(PyMethodDef* ml, PyObject* data)

PyCFunction_New 可能会创建一个 Callable-Type PyObject,以函数(包装在 ml 中)和附加数据(包装在 self 中)作为初始值

第二个参数可以是任何东西,实际上它甚至不需要是 PyObject*。当 Python 执行封装在 ml 中的函数时,这将是第一个参数。后续参数取决于 ml->ml_flags,如下所述。

第一个参数是一个 PyMethodDef 对象,我们可以用它来封装一个函数。

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

因此,它包含一个(特定的)函数指针:

typedef PyObject *(*PyCFunction)(PyObject*, PyObject*);

……还有一面旗帜,

/* Flag passed to newmethodobject */
/* #define METH_OLDARGS  0x0000   -- unsupported now */
#define METH_VARARGS  0x0001
#define METH_KEYWORDS 0x0002
/* METH_NOARGS and METH_O must not be combined with the flags above. */
#define METH_NOARGS   0x0004
#define METH_O        0x0008

https://docs.python.org/3.4/c-api/structures.html

我们可以通过这种方式将 3 种函数传递给 Python:

PyObject*foo( PyObject* data )                                 // ml_meth=METH_NOARGS
PyObject*foo( PyObject* data, PyObject* args )                 // ml_meth=METH_VARARGS
PyObject*foo( PyObject* data, PyObject* args, PyObject* kwds ) // ml_meth=METH_KEYWORDS

编辑:https ://docs.python.org/3/tutorial/classes.html#method-objects

如果您仍然不了解方法的工作原理,那么查看实现也许可以澄清问题。当引用不是数据属性的实例属性时,将搜索其类。如果名称表示作为函数对象的有效类属性,则通过将实例对象和刚刚在抽象对象中找到的函数对象打包(指向)来创建方法对象:这就是方法对象。当使用参数列表调用方法对象时,会从实例对象和参数列表构造一个新的参数列表,并使用这个新的参数列表调用函数对象。

于 2014-11-03T18:16:55.183 回答
1

在寻找这两个函数的文档时,我也有同样的问题。谷歌搜索我找到了这个链接:https ://github.com/python/cpython/blob/main/Objects/methodobject.c

看到 CPython 代码,我发现更容易理解这个主题。

PyObject *
PyCFunction_New(PyMethodDef *ml, PyObject *self)
{
    return PyCFunction_NewEx(ml, self, NULL);
}

PyObject *
PyCFunction_NewEx(PyMethodDef *ml, PyObject *self, PyObject *module)
{
    return PyCMethod_New(ml, self, module, NULL);
}

PyObject *
PyCMethod_New(PyMethodDef *ml, PyObject *self, PyObject *module, PyTypeObject *cls)
{
    /* Figure out correct vectorcall function to use */
    vectorcallfunc vectorcall;
    switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
                            METH_O | METH_KEYWORDS | METH_METHOD))
    {
        case METH_VARARGS:
        case METH_VARARGS | METH_KEYWORDS:
            /* For METH_VARARGS functions, it's more efficient to use tp_call
             * instead of vectorcall. */
            vectorcall = NULL;
            break;
        case METH_FASTCALL:
            vectorcall = cfunction_vectorcall_FASTCALL;
            break;
        case METH_FASTCALL | METH_KEYWORDS:
            vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS;
            break;
        case METH_NOARGS:
            vectorcall = cfunction_vectorcall_NOARGS;
            break;
        case METH_O:
            vectorcall = cfunction_vectorcall_O;
            break;
        case METH_METHOD | METH_FASTCALL | METH_KEYWORDS:
            vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS_METHOD;
            break;
        default:
            PyErr_Format(PyExc_SystemError,
                         "%s() method: bad call flags", ml->ml_name);
            return NULL;
    }

PyCFunction_New 和 PyCFunction_NewEx 只是 PyCMethod_New 的特例。

于 2021-11-19T15:57:44.170 回答