Python方法访问器每次访问都会创建新的对象?

11
在调查一个其他问题时,我发现了以下内容:
>>> class A:
...   def m(self): return 42
... 
>>> a = A()

这是预料中的情况:

>>> A.m == A.m
True
>>> a.m == a.m
True

但是这个我没有预料到:

>>> a.m is a.m
False

尤其不要出现这种情况:

>>> A.m is A.m
False

Python似乎会为每个方法访问创建新的对象。我为什么会看到这种行为?也就是说,为什么它不能每个类和每个实例重用一个对象?

2个回答

15

是的,Python为每次访问创建新的方法对象,因为它构建了一个包装器对象来传递self。这被称为绑定方法

Python使用描述符来实现这一点;函数对象有一个__get__方法,当在类上访问时调用该方法:

>>> A.__dict__['m'].__get__(A(), A)
<bound method A.m of <__main__.A object at 0x10c29bc10>>
>>> A().m
<bound method A.m of <__main__.A object at 0x10c3af450>>
注意Python无法重用A().m。Python是一种高度动态的语言,访问.m本身可能会触发更多的代码,这会改变下一次访问A().m时返回的行为。 @classmethod@staticmethod装饰器利用这个机制,分别返回绑定到类的方法对象和未绑定的函数。
>>> class Foo:
...     @classmethod
...     def bar(cls): pass
...     @staticmethod
...     def baz(): pass
... 
>>> Foo.__dict__['bar'].__get__(Foo(), Foo)
<bound method type.bar of <class '__main__.Foo'>>
>>> Foo.__dict__['baz'].__get__(Foo(), Foo)
<function Foo.baz at 0x10c2a1f80>
>>> Foo().bar
<bound method type.bar of <class '__main__.Foo'>>
>>> Foo().baz
<function Foo.baz at 0x10c2a1f80>

详见Python 描述符指南了解更多内容。

然而,Python 3.7 新增了一个 LOAD_METHOD - CALL_METHOD 操作码对,用来替代当前的 LOAD_ATTRIBUTE - CALL_FUNCTION 操作码对。这种优化旨在避免每次创建新的方法对象。该优化将执行路径从 type(instance).__dict__['foo'].__get__(instance, type(instance))() 转换为 type(instance).__dict__['foo'](instance),因此直接向函数对象“手动”传递实例。如果找到的属性不是纯 Python 函数对象,则该优化会回退到正常的属性访问路径(包括绑定描述符)。


我明白了,这实际上是我正在调查的内容。然而,我原本期望它至少会为每个静态调用重复使用对象。 - Krumelur
@Krumelur:为什么会这样呢?Python是一种动态语言,访问a.m可能会触发代码,从而替换下一次访问的m - Martijn Pieters
这样说起来很有道理,但它违背了我的直觉感觉。 - Krumelur

7
因为这是实现绑定方法最方便、最简单而且占用内存最少的方式。
如果您不知道,绑定方法指的是能够像这样做某事:
f = obj.m
# ... in another place, at another time
f(args, but, not, self)

函数是描述符。描述符是通用的对象,当作为类或对象的属性访问时可以表现出不同的行为。它们用于实现propertyclassmethodstaticmethod和其他一些东西。函数描述符的具体操作是,对于类访问,它们返回自身,对于实例访问,则返回一个新的绑定方法对象。(实际上,这只适用于Python 3; 在这方面,Python 2 更为复杂,它有“未绑定方法”,这基本上是函数,但并非完全如此)。

每次访问都创建一个新对象的原因是为了简单和高效:为每个实例的每个方法预先创建绑定方法需要时间和空间。在需要时创建它们并永远不释放它们是潜在的内存泄漏(尽管CPython对于其他内置类型做了类似的事情),在某些情况下稍微慢一些。复杂的基于弱引用的缓存方案方法对象也不是免费的,而且更为复杂(从历史上看,绑定方法比弱引用早得多)。


这绝对是有道理的,我也预料到了会有这样的答案。我本来以为方法调用需要非常快,而对象创建则太慢了。但是迭代方法调用的性能并不是 Python 的优势之一。 - Krumelur
我特别喜欢“最不神奇”的方法。我希望其他人也能更多地考虑这种解决方案 :) - Krumelur
@Krumelur,你说得对,方法调用比大多数语言慢,但这只是部分由于绑定方法对象。属性查找和普通函数调用本身就已经相对昂贵,至少在CPython和PyPy中,“方法缓存”可以减少成本。 - user395760

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