当 Python __call__ 方法获得额外的第一个参数时?

Posted

技术标签:

【中文标题】当 Python __call__ 方法获得额外的第一个参数时?【英文标题】:When the Python __call__ method gets extra first argument? 【发布时间】:2019-11-29 05:37:10 【问题描述】:

以下示例

import types
import pprint
class A:
    def __call__(self, *args):
        pprint.pprint('[A.__call__] self=%r, args=%r'
                      % (self, list(args)))
class B:
    pass
if __name__ == '__main__':
    a = A()
    print(callable(a))
    a(1, 2)
    b = B()
    b.meth = types.MethodType(a, b)
    b.meth(3, 4)

打印

True
'[A.__call__] self=<__main__.A object at 0xb7233c2c>, args=[1, 2]'
('[A.__call__] self=<__main__.A object at 0xb7233c2c>, args=[<__main__.B '
 'object at 0xb71687cc>, 3, 4]')

__call__ 方法参数的数量在 b.meth(3, 4) 示例。请解释第一个(__main__.B object...) 以及 Python 何时提供它?

在 Debian 9.9 Stretch 上使用 Python 3.5.3

【问题讨论】:

【参考方案1】:

这里的重要概念是,类函数是一个将“self”作为第一个参数绑定到它的函数。

我将通过几个示例进行演示。以下代码对于所有示例都是相同的:

import types

# Class with function
class A:
    def func(*args):
        print('A.func(%s)'%(', '.join([str(arg) for arg in args])))

# Callable function-style class
class A_callable:
    def __call__(*args):
        print('A_callable.__call__(%s)'%(', '.join([str(arg) for arg in args])))

# Empty class
class B():
    pass

# Function without class
def func(*args):
    print('func(%s)'%(', '.join([str(arg) for arg in args])))

现在让我们考虑几个例子:

>>> func(42)
func(42)

这个很明显。它只是使用参数42 调用函数func

接下来的更有趣:

>>> A().func(42)
A.func(<__main__.A object at 0x7f1ed9ed2908>, 42)
>>> A_callable()(42)
A_callable.__call__(<__main__.A_callable object at 0x7f1ed9ed28d0>, 42)

您可以看到类对象self 自动作为第一个参数提供给函数。需要注意的是,不是添加了 self 参数,因为函数存储在对象中,而是因为函数是作为对象的一部分构造的,并且因此对象绑定到它

演示:

>>> tmp = A().func
>>> tmp
<bound method A.func of <__main__.A object at 0x7f1ed9ed2978>>
>>> tmp(42)
A.func(<__main__.A object at 0x7f1ed9ed2978>, 42)

>>> tmp = A_callable().__call__
>>> tmp
<bound method A_callable.__call__ of <__main__.A_callable object at 0x7f1ed9ed2908>>
>>> tmp(42)
A_callable.__call__(<__main__.A_callable object at 0x7f1ed9ed2908>, 42)

self 参数确实没有被添加,因为您在它前面写了a.。它是函数对象本身的一部分,将其存储在变量中仍然保持该绑定。

您也可以手动将类对象绑定到函数,如下所示:

>>> tmp = types.MethodType(func, B)
>>> tmp
<bound method func of <class '__main__.B'>>
>>> tmp(42)
func(<class '__main__.B'>, 42)

另一方面,仅仅将一个函数分配给一个类并self绑定到该函数。如前所述,参数不是在调用时动态添加,而是在构造时静态添加:

>>> b = B()
>>> b.func = func
>>> b.func
<function func at 0x7f1edb58fe18>
>>> b.func(42)
func(42) # does NOT contain the `self` argument

这就是为什么我们需要明确地将self 绑定到函数,如果我们想将它添加到一个对象:

>>> b = B()
>>> b.func = types.MethodType(func, b)
>>> b.func
<bound method func of <__main__.B object at 0x7f1ed9ed2908>>
>>> b.func(42)
func(<__main__.B object at 0x7f1ed9ed2908>, 42)

剩下的就是了解绑定是如何工作的。如果一个方法func 绑定了一个参数a,并被*args 调用,它会将a 添加到*args开头,然后将其传递给功能。 开始在这里很重要。


现在我们知道了理解您的代码所需的一切:

>>> a = A_callable()
>>> b = B()
>>> b.func = types.MethodType(a, b)
>>> b.func
<bound method ? of <__main__.B object at 0x7f1ed97e9fd0>>
>>> b.func(42)
A_callable.__call__(<__main__.A_callable object at 0x7f1ed97fb2b0>, <__main__.B object at 0x7f1ed97e9fd0>, 42)

首先,我们可以将b.func 更改为普通的tmp,因为如前所述,向对象添加函数不会改变其类型或功能。只有绑定 self 才可以。

然后,让我们一步一步地看代码:

>>> a = A_callable()
>>> b = B()

到目前为止一切顺利。我们有一个空对象b 和一个可调用对象a

>>> tmp = types.MethodType(a,b)

这条线是症结所在。如果你明白这一点,你就会明白一切。

tmp 现在是函数a,绑定了b。这意味着,如果我们调用tmp(42),它会将b 添加到其参数的开头a 将因此收到 b, 42。然后,因为a 是可调用的,它会将其参数转发给a.__call__

这意味着,我们处于tmp(42) 等于a.__call__(b, 42) 的位置。

因为__call__A_callable的类函数,所以aa的构造过程中会自动绑定到__call__函数。因此,在参数到达A_callable.__call__ 之前,a 被添加到参数列表的开头,这意味着参数现在是a, b, 42

现在我们处于tmp(42) 等于A_callable.__call__(a, b, 42) 的位置。这正是您所看到的:

>>> tmp = types.MethodType(a, b)
>>> tmp(42)
A_callable.__call__(<__main__.A_callable object at 0x7f1ed97fb2b0>, <__main__.B object at 0x7f1ed97e9fd0>, 42)
>>> A_callable.__call__(a, b, 42)
A_callable.__call__(<__main__.A_callable object at 0x7f1ed97fb2b0>, <__main__.B object at 0x7f1ed97e9fd0>, 42)

现在,如果您将参数拆分为self, *args,您基本上只需将第一个参数取出并将其存储在self 中。你的第一个参数是a,所以self 将是a,而你的另一个*args 将是b, 42。同样,这正是您所看到的。

【讨论】:

以上是关于当 Python __call__ 方法获得额外的第一个参数时?的主要内容,如果未能解决你的问题,请参考以下文章

python__init__和__call__

python3--__call__拦截调用

033.Python的__del__析构方法he__call__方法

python 之简单聊聊 析构函数和特殊的__call__方法

python __del__方法(析构函数)和垃圾回收机制__call__方法和可调用对象

当 `func = func.__call__` 被执行时,Python 会发生啥?