gpt4 book ai didi

python - 为什么 `PyObject_GenericSetAttr` 使用 lambda 时的行为与命名函数不同

转载 作者:行者123 更新时间:2023-12-01 09:09:35 25 4
gpt4 key购买 nike

我最近正在尝试内置类型的猴子修补(是的,我知道这是一个糟糕的想法——相信我,这仅用于教育目的)。

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

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

不同之处在于,absPyCFunctionObject 类型的内置函数。 ,而 lambda 的类型为 PyFunctionObject (与 PyCFunction... 相比,缺少一个 C)。

这些函数不能用于修补,例如 PEP-579 .

PEP-579 中也提到了这个问题,即 cython 函数是 PyC 函数,因此被视为内置函数:

%%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 {

在字典_PyType_Lookup(tp, name)中查找函数(此处为descr)后,method_found仅设置为1如果找到的函数属于 PyFunction 类型,但内置 PyC 函数则不是这种情况。因此,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 正确指出的那样,如果 PyCFunctionObject 支持描述符协议(protocol)(更准确地说是提供 tp_descr_get),即使在 meth_found = 0 之后; 它仍然会有一个回退,这会导致所需的行为。 PyFunctionObject does provide it ,但是 PyCFunctionObject does not .

旧版本使用 LOAD_ATTR+CALL_FUNCTION 来表示 a.fun(),为了工作,函数对象必须支持描述符协议(protocol)。但现在好像不强制了。

我使用 PyCFunction_Check(descr) 将关键行扩展到:

 if (PyFunction_Check(descr) || PyCFunction_Check(descr) ||
(Py_TYPE(descr) == &PyMethodDescr_Type))

已经表明,内置方法也可以用作绑定(bind)方法(至少对于上面的情况)。但这可能会破坏一些东西 - 我没有运行任何更大的测试。

但是,正如@user2357112提到的(再次感谢),这会导致不一致,因为meth = foo.bar仍然使用LOAD_ATTR,因此取决于描述符协议(protocol)。

<小时/>

推荐:我找到了this answer有助于理解 LOAD_ATTR 的情况。

关于python - 为什么 `PyObject_GenericSetAttr` 使用 lambda 时的行为与命名函数不同,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51772066/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com