为什么同一类的不同对象的方法具有相同的id?

10
在下面这段代码中,我不明白为什么useless_func在属于两个不同对象时却具有相同的id?
class parent(object):
   @classmethod
   def a_class_method(cls):
     print "in class method %s" % cls

   @staticmethod
   def a_static_method():
     print "static method"

   def useless_func(self):
     pass

 p1, p2 = parent(),parent()

 id(p1) == id(p2) // False

 id(p1.useless_func) == id(p2.useless_func) // True

可能是https://dev59.com/c2Yr5IYBdhLWcg3w29pI的重复问题。 - Ankit Jaiswal
@AnkitJaiswal 这是不同的。 - jamylak
2个回答

13

这是一个非常有趣的问题!

在你的条件下,它们看起来是一样的:

Python 2.7.2 (default, Oct 11 2012, 20:14:37) 
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class Foo(object):
...   def method(self): pass
... 
>>> a, b = Foo(), Foo()
>>> a.method == b.method
False
>>> id(a.method), id(b.method)
(4547151904, 4547151904)

但需要注意的是,一旦对它们进行任何操作,它们就会变得不同:

>>> a_m = a.method
>>> b_m = b.method
>>> id(a_m), id(b_m)
(4547151*9*04, 4547151*5*04)

然后,当再次测试时,它们又发生了变化!

>>> id(b.method)
4547304416
>>> id(a.method)
4547304416

当访问实例上的方法时,将返回“绑定方法”的实例。绑定方法存储对实例和方法函数对象的引用:

>>> a_m
<bound method Foo.method of <__main__.Foo object at 0x10f0e9a90>>
>>> a_m.im_func is Foo.__dict__['method']
True
>>> a_m.im_self is a
True
请注意,我需要使用 Foo.__dict__['method'],而不是 Foo.method,因为 Foo.method 会产生一个"未绑定方法" … 这个目的留给读者自己思考。
这个“绑定方法”对象的目的是使方法像函数一样在传递时表现得“合理”。例如,当我调用函数 a_m() 时,那就等同于调用 a.method(),尽管我们不再有对 a 的显式引用。与之相比,在 JavaScript(例如)中,var method = foo.method; method() 并不能产生与 foo.method() 相同的结果。
因此,我们回到最初的问题:为什么看起来 id(a.method)id(b.method) 返回相同的值呢?我认为 Asad 是正确的:这与 Python 的引用计数垃圾收集器有关*:当评估表达式 id(a.method) 时,会分配一个绑定方法,计算 ID,然后释放绑定方法。当分配下一个绑定方法——b.method 的绑定方法——时,它被分配到了完全相同的内存位置,因为自从分配 a.method 的绑定方法以来没有发生任何(或有平衡数量的)分配。这意味着 a.method 看起来具有与 b.method 相同的内存位置。
最后,这解释了为什么第二次检查时内存位置似乎发生了变化:在第一次和第二次检查之间进行的其他分配意味着它们第二次被分配到不同的位置(注意:它们被重新分配是因为所有对它们的引用都丢失了;绑定方法被缓存†,因此访问相同的方法两次将返回相同的实例:a_m0 = a.method; a_m1 = a.method; a_m0 is a_m1 => True)。
*: 吹毛求疵的人注意:实际上,这与实际的垃圾收集器无关,后者只存在于处理循环引用时……但是……那是另一个故事。†:至少在 CPython 2.7 中是这样;CPython 2.6 似乎没有缓存绑定方法,这使我期望该行为未被指定。

2
我相信在你的第二个例子中,你有两个不同的引用,但是a.method和b.method仍然具有相同的id。 - Hamish
1
@Hamish 这不是 id 的工作方式:a = [] b = a id(a) == id(b) True - Patashu
2
lst = [1,2,3]; len({id(lst2.append) for _ in range(1000000)}) 实际上在不同的运行中产生不同的结果(通常在1-3之间)...应该查看源代码以了解它是如何处理的... - root
1
实际上,只有在运行于IPython控制台时才出现不一致的情况...(我认为这可能是在幕后进行一些其他调用?) - root
1
@DavidWolever 绑定方法被缓存,因此两次访问相同的方法将返回相同的实例 a_m0 = a.method; a_m1 = a.method; a_m0 is a_m1 => True。但这并不一定是正确的。我使用的是2.6.5 (r265:79063, Apr 16 2010, 13:57:41) [GCC 4.4.3]。 - Ankur Agarwal
显示剩余6条评论

9
以下是我认为正在发生的事情:
  1. 当你取消引用 p1.useless_func 时,会在内存中创建一个副本。这个内存位置由 id 返回。
  2. 由于没有对刚刚创建的方法副本的引用,它会被GC回收,并且该内存地址再次可用。
  3. 当你取消引用 p2.useless_func 时,在相同的内存地址中创建了一个副本 (它是可用的),你再次使用 id 检索到它。
  4. 第二个副本被回收。

如果你运行大量其他代码并再次检查实例方法的 id,我打赌这些 id 将与彼此相同,但与原始运行不同。

另外,你可能会注意到在David Wolver的示例中,一旦获得方法副本的持久引用,这些 id 就变得不同。

为了确认这个理论,这里有一个使用 Jython (与 PyPy 相同的结果) 的 shell 会话:

Jython 2.5.2 (Debian:hg/91332231a448, Jun 3 2012, 09:02:34) 
[OpenJDK Server VM (Oracle Corporation)] on java1.7.0_21
Type "help", "copyright", "credits" or "license" for more information.
>>> class parent(object):
...     def m(self):
...             pass
... 
>>> p1, p2 = parent(), parent()
>>> id(p1.m) == id(p2.m)
False

3
啊,我们有了获胜者!那太有道理了。非常棒的答案。 - David Wolever
@jamylak 这是一个非常慷慨的编辑。事实上,我认为那里有足够的内容来证明需要一个单独的答案。 - Asad Saeeduddin
@Asad 我不这么认为,它应该属于这里。 - jamylak

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