python属性访问顺序_Python属性查找顺序分析

Python内部是怎么去查找属性的呢?虽然我也写过不少Python代码,但是涉及到比较底层的东西,有时候还是比较茫然。这里看一下,Python怎么从一个对象里去查找属性。

类型与实例

面向对象

这里用“类型”来代表面向对象里的类,英文对应type或class;类实例化得到的对象叫“实例”,英文对应instance或object。

class A:

m = 10

def f(self):

pass

a = A()

这里的A是类型,a是实例。可以由实例获取类型,即a.__class__ is A。

Python的类型和实例,都有一个__dict__,记录了里面包含的内容。而Python的“面向对象”的底层实现,表面看上去就是两点:按照合适的顺序在一串__dict__里找属性;

在合适的时候应用descriptor protocol。

类型的__dict__

一个类里面可以有很多东西,这些东西都放在了类型的__dict__里。例如上面的类型A,里面有个m,有个f,可以直接用A.__dict__查看:

A.__dict__

mappingproxy({'__module__': '__main__',

'm': 10,

'f': ,

'__dict__': ,

'__weakref__': ,

'__doc__': None})

可见这个mappingproxy里面有m和f。

实例的__dict__

实例也有__dict__,例如上面的a,也可以直接查看内容:

a.__dict__

{}

返回了一个空字典,说明这个a实例本身确实没有记录什么东西。

简单的查找

a里面什么都没有,那么为什么可以用a访问m和f呢?先看最简单的情况,用a.m访问类变量m:

a.m

10

当然是可以访问的。这里,属性查找就有:一般,如果实例里面没有,就去类型里面找。

那如果实例里面有呢?

a.__dict__['m'] = 20

a.m

20

A.__dict__['m']

10

这时,a.__dict__里有了新的m,直接用a.m的时候,直接读到的就是实例里面的属性,再没有去类型里面找了。这样属性查找就有:一般,如果实例里面有,就用实例里面的,不会再去类型里找。

Descriptor Protocal

获取简单的变量的时候就是按上面所说的顺序,但是获取复杂的东西,例如一个函数的时候,descriptor就要发挥作用了。

实例方法

对实例a调用f函数,会调用A.f,且第一个参数self会是实例a本身。这底层用到了descriptor protocol,简单说就是,当要获取的这个属性实际上是一个descriptor的时候,就执行这个descriptor的__get__函数,调用的时候把实例和类型都传给这个__get__,最后把__get__的返回值作为最终属性返回。

Python的函数,就是descriptor的一种。

A.f

a.f

>

用a.f去获取f时,返回的并不是function,而是一个bound method。底层上,是先查找实例的__dict__,里面没有名字是f的东西;然后查找类型的__dict__,里面有个名字是f的东西,并且这个东西是个descriptor;然后就调用这个descriptor的__get__,把__get__的返回值作为属性查找的最终结果。对函数,就是把实例和普通函数绑定到一起,得到一个bound method,调用的时候,会把实例当作普通函数的第一个参数传进去。

这样可以达到同样的效果:

A.__dict__['f'].__get__(a, A)

>

或者按照Python文档里所描述的transforms b.x into type(b).__dict__['x'].__get__(b, type(b))

实际上是在类型里找到的,不过因为是个descriptor,于是多执行了一步__get__。

实例先于类型

这里也可以验证下,对函数这种descriptor来说,首先查找实例的__dict__,然后才查找类型的__dict__。

a.__dict__['f'] = 'abc'

a.f

'abc'

可见,实例里的名字覆盖了类型里的名字。

实例里的函数无法触发descriptor

如果把a里面的f替换成一个正常的函数呢,可以override掉A.f么?试了一下,无法触发descriptor,所以粗略地说并不可以:

def f2(self):

print('abc')

a.__dict__['f'] = f2

a.f

成了一个简单的function,并不是bound method,那么实例a并不能自动成为f2函数里的self。不过可以手动把self bind进目标函数的第一个参数,然后塞到实例__dict__里。

data-descriptor

然后是唯一的特殊情况,就是data-descriptor。

@property就会构造一个data-descriptor。

class B:

@property

def n(self):

return 42

b = B()

用实例b读取一下n:

b.n

42

按照顺序,实例b里面没有n,于是就会类型B里查找,果然有,而且恰好是个descriptor,然后调用这个descriptor的__get__函数,property.__get__会调用def n函数,并且将返回值作为最终的属性值。

这不和类里的函数一模一样么!确实一模一样,除非,实例b里也有一个名字为n的东西:

b.__dict__['n'] = 9

b.n

42

为什么实例里明明有一个n,返回的还是42呢?这就是不一样的地方:如果类型里有,并且是个data descriptor,那么就必然使用这个data descriptor,即使实例里有同名属性也不会覆盖类型里的data descriptor。

其实这个特殊情况是有一定道理的,可以想象更新一个property的值,例如执行b.n = 84,如果按朴素的流程,会在实例b的__dict__里面加一个新的n,但这是错误的,实际上应该执行类型B的n.setter(84)。猜测,既然设置data-descriptor会跳过实例,直接调用类型里的函数,那么读取data-descriptor的时候,也跳过实例吧。

__getattr__和__getattribute__

如果实例里没有,类型里也没有,就会调用__getattr__,给用户一个机会处理读取“不存在”的东西,大致对应C里的tp_getattr。这个也是非常有用的,最适合做proxy,或者动态计算属性。例如自动调用rpc。

__getattribute__和__getattr__不一样。本文描述的属性查找顺序,正是__getattribute__需要完整实现的,对应C里的tp_getattro。而__getattr__只有__getattribute__失败的时候才会试图调用。

从__getattribute__失败后,fallback到__getattr__,应该是typeobject.c里的slot_tp_getattr_hook处理的。这个slot_tp_getattr_hook里面,先调用__getattribute__,如果抛出了AttributeError(有PyErr_ExceptionMatches(PyExc_AttributeError)这句判断),就清掉这个异常,再调用脚本里的__getattr__作为结果(PyErr_Clear(); res = call_attribute(self, getattr, name);)。

__mro__

上面都是用简单的类型来描述的,如果涉及到继承层级会稍微复杂一些。例如存在继承链条或者多重继承。但是这里反而很简单,只要将“在这个一个类型里查找”,替换成“在__mro__里的这一串类型里查找”即可。对应C里的_PyType_Lookup:Internal API to look for a name through the MRO.

优先级顺序

到这里,就可以总结出默认的属性查找的顺序了,也就是object.__getattribute__实现的逻辑:类型__dict__里的data descriptor;

实例__dict__里的任何东西;

类型__dict__里的non-data descriptor;

类型__dict__里的其他任何东西。

其实这里的3和4可以并为一条,就是“类型__dict__里除了data descriptor之外的东西”。

虽然不是在__getattribute__里实现的,但是在更外层看来,可以加一条footnote:__getattribute__失败后,试图调用用户提供的__getattr__。

C实现源码解析

寻找关键函数

首先要找到这个函数,可以看文档,或者干脆边试边在代码里找。这里借助dis来看a.m对应的操作:

import dis

dis.dis('a.m')

1 0 LOAD_NAME 0 (a)

2 LOAD_ATTR 1 (m)

4 RETURN_VALUE

看来是这个LOAD_ATTR的opcode。去ceval.c里找到这个opcode:

case TARGET(LOAD_ATTR): {

PyObject *name = GETITEM(names, oparg);

PyObject *owner = TOP();

PyObject *res = PyObject_GetAttr(owner, name);

Py_DECREF(owner);

SET_TOP(res);

if (res == NULL)

goto error;

DISPATCH();

}

应该就是这里的PyObject_GetAttr,然后打断点单步调试一下,就会进到默认的object.__getattribute__的实现了:object.c里的PyObject_GenericGetAttr。这个函数直接调用了_PyObject_GenericGetAttrWithDict,如下:

PyObject *

PyObject_GenericGetAttr(PyObject *obj, PyObject *name)

{

return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);

}

源码注解

删除一些错误处理和引用计数,只看关键步骤。注意参数里的*dict为空。

PyObject *

_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,

PyObject *dict, int suppress)

{

PyTypeObject *tp = Py_TYPE(obj); // 实例的类型 PyObject *descr = NULL; // 在类型里找到的东西,可能是descriptor;可能为空 PyObject *res = NULL; // 最终结果 descrgetfunc f; // descriptor的__get__;可能为空 Py_ssize_t dictoffset;

PyObject **dictptr;

// 在类型mro里根据name找东西,结果可能是descriptor,可能是NULL,可能是其他东西 descr = _PyType_Lookup(tp, name);

f = NULL;

if (descr != NULL) {

// 在类型里找到东西了 // 记录找到的这个东西的__get__,可能为空。 f = Py_TYPE(descr)->tp_descr_get;

if (f != NULL && PyDescr_IsData(descr)) {

// 【1】__get__不为空,是descriptor!并且IsData为true,是data descriptor! // 调用__get__,其返回值作为最终结果 res = f(descr, obj, (PyObject *)Py_TYPE(obj));

goto done;

}

}

// 如果没传obj的dict指针进来,这里就自己找一下 if (dict == NULL) {

/* Inline _PyObject_GetDictPtr */

dictptr = (PyObject **) ((char *)obj + dictoffset);

dict = *dictptr;

}

if (dict != NULL) {

// 实例有__dict__,去里面根据name找东西 res = PyDict_GetItemWithError(dict, name);

if (res != NULL) {

// 【2】在实例的dict里找到东西了,直接作为结果 goto done;

}

}

if (f != NULL) {

// 【3】类型里找到的东西有__get__,在这里一定只能是个non-data descriptor // 调用__get__,返回值作为结果 res = f(descr, obj, (PyObject *)Py_TYPE(obj));

goto done;

}

if (descr != NULL) {

// 【4】类型里找到的东西没有__get__,那这个东西本身作为结果返回 res = descr;

descr = NULL;

goto done;

}

// 找了一串都没有,只能报错了,最常见的“object has no attribute” if (!suppress) {

PyErr_Format(PyExc_AttributeError,

"'%.50s' object has no attribute '%U'",

tp->tp_name, name);

}

done:

return res;

}

注意_PyType_Lookup,在mro里,或者说一串类型里找name对应的东西。

特别注意注释里面的【1】【2】【3】【4】,分别对应了查找属性的几种情况,这里再列一下:类型__dict__里的data descriptor;

实例__dict__里的任何东西;

类型__dict__里的non-data descriptor;

类型__dict__里的其他任何东西。

关键文档

其实这些分析,在文档里都是有迹可循的,甚至可以说文档里写的清清楚楚。这里列一下关键的位置。Python HOWTOs - Descriptor HowTo Guide

两种descriptor的顺序Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. ...

属性查找优先级The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to __getattr__() if provided.

The Python Language Reference - Data model - The standard type hierarchy - Invoking Descriptors

默认操作实例__dict__里的东西The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary.

如果类型是descriptor,则应用descriptor protocolHowever, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead.

方法是non-data descriptor,可以被实例覆盖Python methods (including staticmethod() and classmethod()) are implemented as non-data descriptors. Accordingly, instances can redefine and override methods.

property是一种data descriptor,不可以被实例覆盖The property() function is implemented as a data descriptor. Accordingly, instances cannot override the behavior of a property.

其他

性能问题

可以看到,为了找一个属性,Python干了非常多的事情。减少不必要的类型层级,减少不必要的属性访问层级,减少函数调用都有助于提升性能。缓存for里面用到的属性访问链也有效果。

为什么data-descriptor先于实例__dict__

如果一个类里有很多简单属性,那么每次取属性的时候,例如一个人畜无害的self._xxx,都会去类型mro的所有类型的__dict__里去白白找一遍,找不到,然后才去实例__dict__里找,当层级较深的时候,感觉非常浪费。可能是因为,写入属性的时候,data-descriptor必须先于实例__dict__。这个理由有点牵强。个人感觉更合理的一个理由:Python内置类型的属性访问更频繁,而这些内置类型生成的对象甚至可能根本没有__dict__,而是通过GetSetDescriptorType,MemberDescriptorType等,放在了类型的__dict__里,这样优先找类型里的data descriptor会更快。

>>> int.real

>>> type(int.real)

>>> int.real.__get__

>>> int.real.__set__

这也解释了,为什么__slots__会加快属性查找,因为使用slot,就相当于把可以存在实例__dict__里的属性,提升到了类型里,而这里是优先查找的地方。(当然实际内存还是放在了实例里。)

>>> class A:

__slots__ = ('m', 'n')

>>> A.m

>>> type(A.m)

>>> A.m.__get__

>>> A.m.__set__

可见,内置函数和__slots__,都使用了data descriptor,是属性查找的第一站。

但是无法使用实例的__dict__了,即使在__init__里,也无法动态添加属性。除非这样:

>>> class A:

__slots__ = ('m', 'n', '__dict__')

>>> a = A()

>>> a.k = 10

虽然这样会给实例添加__dict__,但是访问__slots__里的属性的时候,仍然会抄近路。而且这样也可以根据需要给实例动态添加属性了。

动态替换类函数

如果有个正在运行的程序,不能重启,但是需要替换一个函数,这就需要hotfix,也就是在不停机的情况下更新类里的一个函数。知道了属性查找的顺序,只需要找到这个类型,把类型__dict__里的对应函数替换掉即可。我猜CPython的method cache应该会正确处理这种情况。当然还是需要小心已经绑定到实例的方法,这些不能自动被替换。需要注意的还有,不能直接给类型的__dict__赋值,因为那是个只读的mappingproxy,可以直接赋值,跳过__dict__。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值