为什么`PyObject_GenericSetAttr`在lambda上的行为与命名函数不同

时间:2018-08-09 16:35:53

标签: python cython cpython

我最近正在尝试使用内置类型的猴子修补程序(是的,我知道这是一个可怕的想法-相信我,这仅是出于教育目的)。

我发现lambda表达式和用def声明的函数之间存在奇怪的区别。看看这个iPython会话:

In [1]: %load_ext cython

In [2]: %%cython
   ...: from cpython.object cimport PyObject_GenericSetAttr
   ...: def feet_to_meters(feet):
   ...: 
   ...:     """Converts feet to meters"""
   ...: 
   ...:     return feet / 3.28084
   ...: 
   ...: PyObject_GenericSetAttr(int, 'feet_to_meters', feet_to_meters)

In [3]: (20).feet_to_meters()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-63ba776af1c9> in <module>()
----> 1 (20).feet_to_meters()

TypeError: feet_to_meters() takes exactly one argument (0 given)

现在,如果我用feet_to_meters包裹lambda,一切正常!

In [4]: %%cython
   ...: from cpython.object cimport PyObject_GenericSetAttr
   ...: def feet_to_meters(feet):
   ...: 
   ...:     """Converts feet to meters"""
   ...: 
   ...:     return feet / 3.28084
   ...: 
   ...: PyObject_GenericSetAttr(int, 'feet_to_meters', lambda x: feet_to_meters(x))

In [5]: (20).feet_to_meters()
Out[5]: 6.095999804928006

这是怎么回事?

1 个答案:

答案 0 :(得分:1)

您的问题可以用Python再现,而没有(非常)肮脏的技巧:

class A:
   pass

A.works = lambda x: abs(1)
A.dont = abs

A().works()  # works
A().dont()   # error

区别在于absPyCFunctionObject类型的内置函数,而lambda是PyFunctionObject类型的(与PyCFunction...相比,缺少C)。

这些功能不能用于修补,例如参见PEP-579

在PEP-579中​​也提到的问题是,cython函数是Py C 函数,因此被视为内置函数:

%%cython
def foo():
    pass

>>> type(foo)
builtin_function_or_method

这意味着,您不能直接将Cython函数用于猴子修补,而必须像已经做的那样将它们包装成lambda或类似的东西。不用担心性能,因为由于方法查找的原因,已经存在开销,多花一点钱并不会改变很多事情。


我必须承认,我不知道为什么会这样(历史上如此)。但是在当前代码(Python3.8)中,您可以轻松地在crucial line中找到_PyObject_GetMethod,这与众不同:

descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        if (PyFunction_Check(descr) ||  # HERE WE GO
                (Py_TYPE(descr) == &PyMethodDescr_Type)) {
            meth_found = 1;
} else {

在字典descr中查找函数(此处为_PyType_Lookup(tp, name))后,只有找到的函数为method_found类型的情况下,PyFunction才设置为1。内置Py C 函数不是这种情况。因此,abs和Co不会被视为方法,而是保持某种“静态方法”。

找到调查起点的最简单方法是检查产生的操作码是否为:

import dis
def f():
  a.fun()

dis.dis(f)

即以下操作码(自Python3.6起似乎已更改):

2         0 LOAD_GLOBAL              0 (a)
          2 LOAD_METHOD              1 (fun)  #HERE WE GO
          4 CALL_METHOD              0
          6 POP_TOP
          8 LOAD_CONST               0 (None)
         10 RETURN_VALUE

我们可以检查ceval.c中的相应部分:

TARGET(LOAD_METHOD) {
            /* Designed to work in tamdem with CALL_METHOD. */
            PyObject *name = GETITEM(names, oparg);
            PyObject *obj = TOP();
            PyObject *meth = NULL;

            int meth_found = _PyObject_GetMethod(obj, name, &meth);
            ....

然后让gdb take us from there


正如@ user2357112正确指出的那样,如果Py C FunctionObject将支持描述符协议(更精确地提供tp_descr_get),即使在meth_found = 0;之后,它仍然具有后退会导致预期的行为。 PyFunctionObject does provide it,但Py C FunctionObject does not

较旧的版本使用LOAD_ATTR的{​​{1}} + CALL_FUNCTION,为了起作用,功能对象必须支持描述符协议。但是现在看来这不是强制性的。

我的快速测试是将a.fun()的关键行扩展为:

PyCFunction_Check(descr)

已经显示,然后内置方法也将作为绑定方法(至少对于上述情况)。但这可能会破坏一些东西-我没有进行任何更大的测试。

但是,正如@ user2357112所提到的(再次感谢),这将导致不一致,因为 if (PyFunction_Check(descr) || PyCFunction_Check(descr) || (Py_TYPE(descr) == &PyMethodDescr_Type)) 仍使用meth = foo.bar并因此取决于描述符协议。


建议:我发现this answer有助于理解LOAD_ATTR的情况。