每当您通过instance.name(在Python 2 中为class.name)查找方法时,都会创建一个新的方法对象。 Python 每次都使用descriptor protocol 将函数包装在一个方法对象中。
因此,当您查找 id(C.foo) 时,会创建一个新的方法对象,您检索其 id(内存地址),然后再次丢弃该方法对象。然后您查找id(cobj.foo),这是一个创建的新方法对象,它重新使用现在释放的内存地址,您会看到相同的值。然后,该方法再次被丢弃(当引用计数降至 0 时收集垃圾)。
接下来,您将对 C.foo 未绑定方法的引用存储在变量中。现在内存地址没有被释放(引用计数为 1,而不是 0),您通过查找 cobj.foo 创建了一个 second 方法实例,该实例必须使用新的内存位置。因此,您会得到两个不同的值。
见documentation for id():
返回对象的“身份”。这是一个整数(或长整数),保证该对象在其生命周期内是唯一且恒定的。 生命周期不重叠的两个对象可能具有相同的id() 值。
CPython 实现细节:这是对象在内存中的地址。
强调我的。
你可以通过类的__dict__属性直接引用函数来重新创建一个方法,然后调用__get__ descriptor method:
>>> class C(object):
... def foo(self):
... pass
...
>>> C.foo
<unbound method C.foo>
>>> C.__dict__['foo']
<function foo at 0x1088cc488>
>>> C.__dict__['foo'].__get__(None, C)
<unbound method C.foo>
>>> C.__dict__['foo'].__get__(C(), C)
<bound method C.foo of <__main__.C object at 0x1088d6f90>>
请注意,在 Python 3 中,整个未绑定/绑定方法的区别已被删除;你会得到一个函数,在你得到一个未绑定的方法之前,你会得到一个方法,否则,一个方法是总是绑定的:
>>> C.foo
<function C.foo at 0x10bc48dd0>
>>> C.foo.__get__(None, C)
<function C.foo at 0x10bc48dd0>
>>> C.foo.__get__(C(), C)
<bound method C.foo of <__main__.C object at 0x10bc65150>>
此外,Python 3.7 添加了一个新的LOAD_METHOD - CALL_METHOD 操作码对,准确地替换了当前的LOAD_ATTRIBUTE - CALL_FUNCTION 操作码对,以避免每次都创建一个新的方法对象。此优化将instance.foo() 的执行路径从type(instance).__dict__['foo'].__get__(instance, type(instance))() 转换为type(instance).__dict__['foo'](instance),因此“手动”将实例直接传递给函数对象。