为什么 `PyObject_GenericSetAttr` 对 lambda 的行为与命名函数不同
Why does `PyObject_GenericSetAttr` behave differently with lambdas than named functions
我最近在尝试使用内置类型的猴子修补(是的,我知道这是一个糟糕的想法——相信我,这仅用于教育目的)。
我发现 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)
现在,如果我用 lambda
包裹 feet_to_meters
,一切正常!
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
这是怎么回事?
您的问题可以在 Python 中重现并且没有(非常)肮脏的技巧:
class A:
pass
A.works = lambda x: abs(1)
A.dont = abs
A().works() # works
A().dont() # error
不同之处在于,abs
是一个类型为 PyCFunctionObject
, while lambda is of type PyFunctionObject
的内建函数(与 PyCFunction...
相比,少了一个 C)。
那些cfunctions不能用于修补,例如参见PEP-579。
在 PEP-579 中也提到了这个问题,即 cython 函数是 PyC 函数,因此被视为内置函数:
%%cython
def foo():
pass
>>> type(foo)
builtin_function_or_method
这意味着,您不能直接使用 Cython 函数进行猴子修补,而必须像您已经做的那样将它们包装到 lambda 或类似函数中。人们不应该担心性能,因为由于方法查找,已经存在开销,再多一点不会显着改变事情。
我必须承认,我不知道为什么会这样(历史上)。但是在当前的代码(Python3.8)中,你可以很容易地找到crucial line in _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 {
在字典 _PyType_Lookup(tp, name)
中查找函数(此处 descr
)后,仅当找到的函数类型为 PyFunction
时,method_found
才设置为 1 ,内置 PyC 函数不是这种情况。因此 abs
和 Co 不被视为方法,而是保持某种 "staticmethod".
找到调查起点的最简单方法是检查生成的操作码:
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);
....
正如@user2357112 正确指出的那样,如果 PyCFunctionObject 支持描述符协议(更准确地说是提供 tp_descr_get
),即使在 meth_found = 0;
它仍然会有后退,这会导致所需的行为。 PyFunctionObject does provide it, but PyCFunctionObject does not.
旧版本对 a.fun()
使用 LOAD_ATTR
+CALL_FUNCTION
,为了工作,函数对象必须支持描述符协议。不过现在好像不是强制的了。
我的快速测试是将 PyCFunction_Check(descr)
的关键行扩展到:
if (PyFunction_Check(descr) || PyCFunction_Check(descr) ||
(Py_TYPE(descr) == &PyMethodDescr_Type))
已经表明,内置方法也可以作为绑定方法使用(至少对于上述情况)。但这可能会破坏某些东西 - 我没有 运行 任何更大的测试。
然而,正如@user2357112 提到的(再次感谢),这会导致不一致,因为 meth = foo.bar
仍然使用 LOAD_ATTR
并因此取决于描述符协议。
建议:我发现 this answer 有助于理解 LOAD_ATTR
的情况。
我最近在尝试使用内置类型的猴子修补(是的,我知道这是一个糟糕的想法——相信我,这仅用于教育目的)。
我发现 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)
现在,如果我用 lambda
包裹 feet_to_meters
,一切正常!
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
这是怎么回事?
您的问题可以在 Python 中重现并且没有(非常)肮脏的技巧:
class A:
pass
A.works = lambda x: abs(1)
A.dont = abs
A().works() # works
A().dont() # error
不同之处在于,abs
是一个类型为 PyCFunctionObject
, while lambda is of type PyFunctionObject
的内建函数(与 PyCFunction...
相比,少了一个 C)。
那些cfunctions不能用于修补,例如参见PEP-579。
在 PEP-579 中也提到了这个问题,即 cython 函数是 PyC 函数,因此被视为内置函数:
%%cython
def foo():
pass
>>> type(foo)
builtin_function_or_method
这意味着,您不能直接使用 Cython 函数进行猴子修补,而必须像您已经做的那样将它们包装到 lambda 或类似函数中。人们不应该担心性能,因为由于方法查找,已经存在开销,再多一点不会显着改变事情。
我必须承认,我不知道为什么会这样(历史上)。但是在当前的代码(Python3.8)中,你可以很容易地找到crucial line in _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 {
在字典 _PyType_Lookup(tp, name)
中查找函数(此处 descr
)后,仅当找到的函数类型为 PyFunction
时,method_found
才设置为 1 ,内置 PyC 函数不是这种情况。因此 abs
和 Co 不被视为方法,而是保持某种 "staticmethod".
找到调查起点的最简单方法是检查生成的操作码:
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);
....
正如@user2357112 正确指出的那样,如果 PyCFunctionObject 支持描述符协议(更准确地说是提供 tp_descr_get
),即使在 meth_found = 0;
它仍然会有后退,这会导致所需的行为。 PyFunctionObject does provide it, but PyCFunctionObject does not.
旧版本对 a.fun()
使用 LOAD_ATTR
+CALL_FUNCTION
,为了工作,函数对象必须支持描述符协议。不过现在好像不是强制的了。
我的快速测试是将 PyCFunction_Check(descr)
的关键行扩展到:
if (PyFunction_Check(descr) || PyCFunction_Check(descr) ||
(Py_TYPE(descr) == &PyMethodDescr_Type))
已经表明,内置方法也可以作为绑定方法使用(至少对于上述情况)。但这可能会破坏某些东西 - 我没有 运行 任何更大的测试。
然而,正如@user2357112 提到的(再次感谢),这会导致不一致,因为 meth = foo.bar
仍然使用 LOAD_ATTR
并因此取决于描述符协议。
建议:我发现 this answer 有助于理解 LOAD_ATTR
的情况。