我们先通过一个简单的类 Point ,考察对象属性的行为:
|
|
Point 类用于表示一个二维坐标,例如 (1, 2) :
|
|
其中, Point 是自定义类对象, p 是 Point 类的实例对象。由于 X 、Y 轴坐标分别作为 x 、y 属性被 init 函数保存于实例对象属性空间中,因而可以通过对象属性查找访问:
|
|
我们在 Point 类中定义了 distance 方法,用于计算坐标到原点的距离。虽然它并不在 p 实例对象的属性空间中,照样可以被 p 查找到,这又是为什么呢?
|
|
我们定义的 distance 方法,明明有一个参数 self ,但我们调用时却无须传递!实际上,你可能早已知晓,Python 自动将 p 作为 self 参数传给 distance 。那么,Python 又是如何暗渡陈仓的呢?
通过观察 p 实例对象 distance 属性,我们找到一些蛛丝马迹:
|
|
我们惊讶地发现,它居然是一个 bound method 对象,而不是我们定义的函数对象!
经过前面章节学习,我们很清楚 distance 方法保存在 Point 类的属性空间,而它只是一个普通的函数对象:
|
|
因此,如果我们通过 Point 类来调用 distance 方法,是需要显式传递 self 参数的:
|
|
那么,distance 函数对象是如何完成到 bound method 对象的华丽转身的呢?Python 在背后又做了哪些不为人知的事情呢?带着这些疑问,我们继续到 Python 虚拟机中寻找答案。
底层布局
在对象模型部分,我们知道对 可调用对象 ( callable )进行调用,Python 内部执行其类型对象的 tp_call 函数。
由于 Point 类对象是类型对象,其类型是 type 。因此,当我们调用 Point 类创建实例对象,Python 将执行 type 的 tp_call 函数,tp_call 函数则回过头来调用 Point 类的 tp_new 函数。
我们并没有给 Point 定义 new 函数,也就是说 Point 类并没有 tp_new 函数。好在 Point 有一个默认基类 object ,而 object 提供了一个兜底的版本:object.tp_new ,它负责为实例对象分配内存。
最后,type.tp_call 将调用 Point.tp_init 函数,也就是我们定义的 init 函数,对实例对象进行初始化。
当 Point 实例对象完成初始化,实例对象与 Point 类对象在底层的内存布局大致是这样的:
p 实例对象背后有一个 dict 对象,用于维护其属性空间,实例属性 x 、 y 便藏身其中;同样,Point 类对象背后也有一个 dict 对象,用于维护其属性空间,属性 z 以及方法 distance 也藏身其中。
字节码
当我们在 p 实例上调用 distance 方法时,由于 p 实例属性空间并没有定义 distance 方法,Python 将到 Point 类的属性空间中查找。如果 Point 类属性空间也没有定义,Python 继续沿着类继承图逐层向上查找。
Python 并没有直接将定义在 Point 类属性空间的 distance 函数对象直接返回,而是对它做了一些手脚。想来这也很合理,如果将 distance 函数对象原样返回的话,我们还要显示传递 self 参数:
|
|
这可真丑陋!那么,Python 为解决这个问题又做了哪些手脚呢?会不会是由一个特殊的字节码完成的呢? 点击“阅读原文”,获取更多详情!
【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: