Python如何区分回调函数是类的成员函数?

31

请看这个简单的示例:

class A:
  def __init__(self, flag):
    self.flag = flag

  def func(self):
    print self.flag

a = A(1)
b = A(2)
callback_a = a.func
callback_b = b.func

callback_a()
callback_b()

结果为:

1
2

代码运行如预期。但我有一个问题。在C语言中,回调函数是以指针的形式传递的。在Python中,应该有一种类似的方式来做到这一点,以便调用者知道函数的地址。但在我的示例中,不仅传递了函数指针,还传递了参数(self),因为同一类的同一方法打印出不同结果。所以我的问题是:

  1. 在Python中,这样的方法只有一个内存副本吗?我的意思是,任何方法的代码都只有一个副本,在我的示例中该方法将不会被克隆自身。我认为它应该只有一个副本,但我仍然提出这个问题是为了获取更多的输入。

  2. 我记得在Python中,每个东西都是一个对象。那么在我的示例中,是否存在具有不同参数但只有一个代码副本的两个函数实例?


1
我认为它会创建一个闭包? - Aram Kocharyan
你应该使用新风格类。将每个子类化自object或它的一个子类。 - aaronasterling
1
@aaronasterling:虽然我不确定它对手头的问题有太大影响? - jdi
@aaronasterling 在这种情况下推荐它甚至没有意义,因为没有指定特定版本的Python。在Python 3.x中,旧式类已经消失了,新式类是默认的。 - Casey Kuball
2
@Darthfett。请注意print语句。 - aaronasterling
显示剩余3条评论
2个回答

24

在Python中,回调函数不仅仅是一个成员函数的引用,而是在创建时与它所指向的对象“绑定”起来的。因此,a.func 创建了一个绑定到 a 的可调用对象,而 b.func 创建了一个绑定到 b 的可调用对象。

Python 只需要在内存中实现一次 func(),但是在运行时很可能会创建一个或多个“跳板”函数以完成绑定(我不确定这方面的内部细节,而且这也会因Python的实现而异)。

如果你打印 id(callback_a)id(callback_b),你会得到不同的结果,说明它们确实是不同的可调用对象。


3
如果你执行 id(b.func) == id(a.func),你会发现它们指向同一块内存。 - jdi
@jdi:你能告诉我为什么 id(a.func) 每次都会给出不同的结果吗? - mshsayem
@mshsayem id 在 CPython 中被实现为对象的内存地址。因此,每次程序运行时它都会在内存中获得不同的位置。 - aaronasterling
因为我们必须在某个地方存储self指针(至少在概念上,但也在现实中 - 对于cpython)。基本上它是一个结构体Py_Object *self, *func,我们显然不能共享该结构体,因为self始终不同。如果您想要实际的函数对象,则可以执行Class.func(您也可以调用它,例如尝试在代码中执行A.func(a)) - 函数对象应该相同(至少在cpython中!) - Voo
4
@jdi:那只是一种具体实现细节,并不能保证。例如,我刚在pypy和ironpython上尝试了一下,它们并不相等。 - DSM
7
尝试使用 a_func = a.func; b_func = b.func; id(a_func) == id(b_func)a.funcb.func 是不同的绑定方法对象。在 CPython 中,绑定方法对象是在属性访问时构造的,并且不会被缓存。当您执行 id(a.func) 时,会构造一个新的绑定方法对象,获取其 id,然后立即对其进行垃圾回收,因为它没有更多的引用。然后,当您执行 id(b.func) 时,新的绑定方法对象将在同一块内存中构造,因此您的相等性是偶然成立的。 - Peter Graham

23

就 CPython 而言,一个函数对象只有一个副本。在实例创建期间,类将其命名空间中的未绑定函数包装为绑定方法。但它们都包装同一个函数。

以下是扩展示例,以显示正在发生的情况。

class A(object):
  def __init__(self, flag):
    self.flag = flag

  def func(self):
    print self.flag

a = A(1)
b = A(2)

callback_a = a.func
callback_b = b.func

print "typeof(callback_a) = {0}".format(type(callback_a))
print "typeof(callback_b) = {0}".format(type(callback_b))

print "typeof(callback_a.__func__) = {0}".format(type(callback_a.__func__))
print "typeof(callback_b.__func__) = {0}".format(type(callback_b.__func__))

print "'callback_a.__func__ is callback_b.__func__'  is {0}".format(callback_a.__func__ is callback_b.__func__)

callback_a()
callback_b()

这段代码输出

typeof(callback_a) = <type 'instancemethod'>
typeof(callback_b) = <type 'instancemethod'>
typeof(callback_a.__func__) = <type 'function'>
typeof(callback_b.__func__) = <type 'function'>
'callback_a.__func__ is callback_b.__func__'  is True

使用is运算符,您可以清楚地看到两个instancemethod类共享同一个函数对象。


2
我想知道为什么这个被踩了。如果有客观的错误,那我想知道。 - aaronasterling
我觉得非常有趣的是,这个 print "'callback_a is callback_b' is {0}".format(callback_a is callback_b) 输出的结果是 false。作为一个来自Javascript和Perl的开发者,我对这种行为感到非常惊讶。是否有Python文档可以更详细地说明这一点? - Kevin G.
5
callback_a is callback_bFalse,因为每个回调函数都是一个 instancemethod,它将函数 实例一起封装为方法。两个回调函数共享相同的 __func__(如初始帖子中所示),但每个实例有不同的 self 对象实例用于调用该函数。明白了吗? - PfhorSlayer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接