PyCFunction_New / PyCFunction_NewEx 文档
我在理解一些关于PyCXX的代码(这是一个C++和Python的桥梁)时遇到了一些困难,主要是关于PyCFunction_New这个函数。
有没有人能解释一下这个函数是怎么工作的?
我从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会把它转换回一个有三个参数的函数,其中第一个参数是args(这和处理函数的_self_and_name_tuple的第一个参数是匹配的)?
而CPython只会从解析'myFunc(7, a=1)'这个调用中知道它实际上是在处理一个关键字函数,也就是有三个参数的函数?
这看起来不太对。
也许CPython会把args1再转换回一个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){...}
现在他们需要通过调用这三个函数中的一个来将这个方法存储在lookup中:
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]和方法本身的细节args1)。处理函数会负责运行这个方法并捕获错误。
注意我们在这个时候并不执行处理函数,而是将这个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注册到lookup中时,我们提供这个处理函数:
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)
2 个回答
我也有过同样的问题,想找这两个函数的文档。通过谷歌搜索,我找到了这个链接: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的特例。
我来试着回答一下:
PyObject* PyCFunction_New(PyMethodDef* ml, PyObject* data)
PyCFunction_New 可能会创建一个可调用的 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
我们可以通过这种方式将三种类型的函数传递给 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
如果你还是不明白方法是怎么工作的,可以看看它的实现,这可能会让事情更清楚。当一个实例属性被引用但不是数据属性时,系统会去查找它的类。如果这个名字对应的确实是一个有效的类属性,并且是一个函数对象,那么就会创建一个方法对象,把实例对象和刚找到的函数对象的指针打包在一起,形成一个抽象对象:这就是方法对象。当调用这个方法对象并传入参数列表时,会根据实例对象和参数列表构建一个新的参数列表,然后用这个新的参数列表来调用函数对象。