10

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

>>> 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 似乎为每个方法访问创建新对象。为什么我会看到这种行为?即它不能重用每个类和每个实例一个对象的原因是什么?

4

2 回答 2

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 描述符 howto

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

于 2014-01-08T17:22:20.300 回答
7

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

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

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

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

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

于 2014-01-08T17:27:24.523 回答