这在很大程度上取决于语言和实现。我会告诉你我对 CPython 和 PyPy 的了解。
一般的想法,这也是 CPython 在大多数情况下所做的,如下所示:
- 每个对象都有一个类,特别是对该类对象的引用。
- 除了显然存储在单个对象中的实例成员之外,该类还具有成员。这包括方法,因此方法没有每个对象的成本。
- 一个类具有由继承关系确定的方法解析顺序 (MRO),其中每个基类只出现一次。如果我们没有多重继承,这只是对基类的引用,但这样一来,MRO 就很难即时计算出来(每次都必须从最派生的类开始)。
- (类也是对象,它们本身也有类,但我们暂时忽略这一点。)
- 如果对象的属性查找失败,则按照 MRO 指定的顺序在 MRO 中的类上查找相同的属性。(这是默认行为,可以通过定义 和 等魔术方法来更改
__getattr__
。__getattribute__
)
到目前为止如此简单,并不能真正解释绑定方法。我只是想确保我们谈论的是同一件事。缺少的部分是描述符。描述符协议在语言参考的“深奥魔法”部分中定义,但简短而简单的故事是,对类的查找可以被它通过方法产生的对象__get__
劫持。更重要的是,该__get__
方法被告知查找是在实例上还是在“所有者”(类)上开始的。
在 Python 2 中,我们有一个丑陋且不必要的UnboundMethod
描述符(除了__get__
方法之外),它只是简单地包装函数以在Class.method(self)
如果self
不是可接受的类型时抛出错误。在 Python 3 中,__get__
它只是所有函数对象的一部分,并且没有绑定方法。在这两种情况下,__get__
当您在类上查找该方法时,该方法会返回自身(因此您可以使用Class.method
,这在少数情况下很有用),当您在对象上查找它时,它会返回一个“绑定方法”对象。这个绑定的方法对象只是存储原始函数和实例,并将后者作为第一个参数传递给前者__call__
(覆盖函数调用语法的特殊方法)。
因此,对于 CPython:虽然绑定方法有成本,但它比您想象的要小。在空间方面只需要两个引用,并且 CPU 成本仅限于小的内存分配,以及调用时的额外间接。请注意,此成本适用于所有方法调用,而不仅仅是那些实际使用绑定方法功能的方法调用。a.f()
必须调用描述符并使用它的返回值,因为在动态语言中,我们不知道它是否经过猴子修补来做一些不同的事情。
在 PyPy 中,事情更有趣。由于它是一种不影响正确性的实现,因此上述模型对于语义推理仍然是正确的。但是,它实际上更快。除了 JIT 编译器在大多数情况下内联然后消除上述整个混乱之外,它们还解决了字节码级别的问题。有两个新的字节码指令,它们保留了语义,但在a.f()
. 还有一个方法缓存可以简化查找过程,但需要一些额外的簿记(尽管其中一些簿记已经为 JIT 完成)。