属性描述符如何影响属性行为

少壮不努力,老大徒伤悲。

—— 汉乐府·《长歌行》

我们先通过一个简单的类 Point ,考察对象属性的行为:

1
2
3
4
5
6
7
class Point:
    z = 0
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def distance(self):
        return math.sqrt(self.x*self.x + self.y*self.y)

Point 类用于表示一个二维坐标,例如 (1, 2)

1
>>> p = Point(1, 2)

其中, Point 是自定义类对象, pPoint 类的实例对象。由于 XY 轴坐标分别作为 xy 属性被 init 函数保存于实例对象属性空间中,因而可以通过对象属性查找访问:

1
2
3
4
5
6
7
8
>>> 'x' in p.__dict__
True
>>> p.x
1

>>> 'y' in p.__dict__
>>> p.y
2

我们在 Point 类中定义了 distance 方法,用于计算坐标到原点的距离。虽然它并不在 p 实例对象的属性空间中,照样可以被 p 查找到,这又是为什么呢?

1
2
3
4
>>> 'distance' in p.__dict__
False
>>> p.distance()
2.23606797749979

我们定义的 distance 方法,明明有一个参数 self ,但我们调用时却无须传递!实际上,你可能早已知晓,Python 自动将 p 作为 self 参数传给 distance 。那么,Python 又是如何暗渡陈仓的呢?

通过观察 p 实例对象 distance 属性,我们找到一些蛛丝马迹:

1
2
>>> p.distance
<bound method Point.distance of <__main__.Point object at 0x1077d4160>>

我们惊讶地发现,它居然是一个 bound method 对象,而不是我们定义的函数对象!

经过前面章节学习,我们很清楚 distance 方法保存在 Point 类的属性空间,而它只是一个普通的函数对象:

1
2
3
4
>>> 'distance' in Point.__dict__
True
>>> Point.distance
<function Point.distance at 0x1077b9730>

因此,如果我们通过 Point 类来调用 distance 方法,是需要显式传递 self 参数的:

1
2
>>> Point.distance(p)
2.23606797749979

那么,distance 函数对象是如何完成到 bound method 对象的华丽转身的呢?Python 在背后又做了哪些不为人知的事情呢?带着这些疑问,我们继续到 Python 虚拟机中寻找答案。

底层布局

在对象模型部分,我们知道对 可调用对象 ( callable )进行调用,Python 内部执行其类型对象的 tp_call 函数。

由于 Point 类对象是类型对象,其类型是 type 。因此,当我们调用 Point 类创建实例对象,Python 将执行 typetp_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 对象,用于维护其属性空间,实例属性 xy 便藏身其中;同样,Point 类对象背后也有一个 dict 对象,用于维护其属性空间,属性 z 以及方法 distance 也藏身其中。

字节码

当我们在 p 实例上调用 distance 方法时,由于 p 实例属性空间并没有定义 distance 方法,Python 将到 Point 类的属性空间中查找。如果 Point 类属性空间也没有定义,Python 继续沿着类继承图逐层向上查找。

Python 并没有直接将定义在 Point 类属性空间的 distance 函数对象直接返回,而是对它做了一些手脚。想来这也很合理,如果将 distance 函数对象原样返回的话,我们还要显示传递 self 参数:

1
p.instance(p)

这可真丑陋!那么,Python 为解决这个问题又做了哪些手脚呢?会不会是由一个特殊的字节码完成的呢? 点击“阅读原文”,获取更多详情!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: