Python方法访问器在每次访问时创建新对象?

时间:2014-01-08 17:18:22

标签: python methods python-internals

在调查another question时,我发现了以下内容:

>>> 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 个答案:

答案 0 :(得分:14)

是的,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 descriptor howto

但是,Python 3.7添加了一个新的LOAD_METHOD - CALL_METHOD操作码对,可以精确地替换当前的LOAD_ATTRIBUTE - CALL_FUNCTION操作码对,以避免每次都创建一个新的方法对象。此优化将instance.foo()的{​​{1}}的执行路径转换为type(instance).__dict__['foo'].__get__(instance, type(instance))(),因此“手动”将实例直接传递给函数对象。如果找到的属性不是纯python函数对象,则优化将回退到普通的属性访问路径(包括绑定描述符)。

答案 1 :(得分:7)

因为这是实现绑定方法的最方便,最不神奇和最节省空间的方法。

如果您不知道,绑定方法是指能够执行以下操作:

f = obj.m
# ... in another place, at another time
f(args, but, not, self)

函数是描述符。描述符是一般对象,当作为类或对象的属性访问时,它们的行为可能不同。它们用于实施propertyclassmethodstaticmethod以及其他一些内容。函数描述符的具体操作是它们返回自己进行类访问,并返回一个新的绑定方法对象以进行实例访问。 (实际上,这只适用于Python 3; Python 2在这方面更复杂,它具有“非绑定方法”,它们基本上是功能但不完全)。

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