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

Kru*_*lur 9 python methods python-internals

在调查另一个问题时,我发现了以下内容:

>>> class A:
...   def m(self): return 42
... 
>>> a = A()
Run Code Online (Sandbox Code Playgroud)

这是预期的:

>>> A.m == A.m
True
>>> a.m == a.m
True
Run Code Online (Sandbox Code Playgroud)

但是,这我也没有想到:

>>> a.m is a.m
False
Run Code Online (Sandbox Code Playgroud)

尤其不是这个:

>>> A.m is A.m
False
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ers 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>>
Run Code Online (Sandbox Code Playgroud)

请注意,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>
Run Code Online (Sandbox Code Playgroud)

有关更多详细信息,请参阅Python描述符howto.

然而,Python的3.7增加了一个新的LOAD_METHOD- CALL_METHOD操作码对,将替换当前LOAD_ATTRIBUTE- CALL_FUNCTION精确地操作码对,以避免每次创建一个新的方法的对象.这种优化变换executon路径instance.foo()type(instance).__dict__['foo'].__get__(instance, type(instance))()type(instance).__dict__['foo'](instance),因此在该实例"手动"直接传递给函数的对象.如果找到的属性不是纯python函数对象,则优化将回退到正常的属性访问路径(包括绑定描述符).


小智 7

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

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

f = obj.m
# ... in another place, at another time
f(args, but, not, self)
Run Code Online (Sandbox Code Playgroud)

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

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