在调查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似乎为每个方法访问创建新对象。为什么我看到这种行为?即为什么它不能每个类重用一个对象,每个实例一个?是什么原因?
答案 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)
函数是描述符。描述符是一般对象,当作为类或对象的属性访问时,它们的行为可能不同。它们用于实施property
,classmethod
,staticmethod
以及其他一些内容。函数描述符的具体操作是它们返回自己进行类访问,并返回一个新的绑定方法对象以进行实例访问。 (实际上,这只适用于Python 3; Python 2在这方面更复杂,它具有“非绑定方法”,它们基本上是功能但不完全)。
在每次访问上创建新对象的原因之一是简单性和高效性:为每个实例的每个方法预先创建绑定方法需要时间和空间。按需创建它们永远不会释放它们是一种潜在的内存泄漏(尽管CPython对其他内置类型做了类似的事情)并且在某些情况下稍慢。复杂的基于弱参数的缓存方案方法对象也不是免费的,而且复杂得多(历史上,绑定方法远远早于弱化)。