python属性查找 深刻理解(attribute lookup)

在Python中,属性查找(attribute lookup)是比较复杂的,特别是涉及到描述符descriptor的时候。html

   

  在上一文章末尾,给出了一段代码,就涉及到descriptor与attribute lookup的问题。而get系列函数(__get__, __getattr__, __getattribute__) 也很容易搞晕,本文就这些问题简单总结一下。python

  首先,咱们知道:缓存

  •     python中一切都是对象,“everything is object”,包括类,类的实例,数字,模块app

  •     任何object都是类(class or type)的实例(instance)ide

  •     若是一个descriptor只实现了__get__方法,咱们称之为non-data descriptor, 若是同时实现了__get__ __set__咱们称之为data descriptor。函数

 

    按照python doc,若是obj是某个类的实例,那么obj.name首先调用__getattribute__。若是类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,而对于描述符(__get__)的调用,则是发生在__getattribute__内部的。官网文档是这么描述的测试

    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.spa

    obj = Clz(), 那么obj.attr 顺序以下:code

    (1)若是“attr”是出如今Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__get__方法, 不然orm

    (2)若是“attr”出如今obj的__dict__中, 那么直接返回 obj.__dict__['attr'], 不然

    (3)若是“attr”出如今Clz或其基类的__dict__中

        (3.1)若是attr是non-data descriptor,那么调用其__get__方法, 不然

        (3.2)返回 __dict__['attr']

    (4)若是Clz有__getattr__方法,调用__getattr__方法,不然

    (5)抛出AttributeError 

  下面是测试代码:

  

 View Code

 

  注意第50行,change_attr给实例的__dict__里面增长了两个属性。经过上下两条print的输出以下:

  Derive object dict {'same_name_attr': 'attr in object', 'not_des_attr': 'I am not descriptor attr'}

  Derive object dict {'same_name_attr': 'attr in object', 'ndd_derive': 'ndd_derive now in object dict ', 'not_des_attr': 'I am not descriptor attr', 'dd_base': 'dd_base now in object dict '}

 

  调用change_attr方法以后,dd_base既出如今类的__dict__(做为data descriptor), 也出如今实例的__dict__, 新航道雅思培训由于attribute lookup的循序,因此优先返回的仍是Clz.__dict__['dd_base']。而ndd_base虽然出如今类的__dict__, 可是由于是nondata descriptor,因此优先返回obj.__dict__['dd_base']。其余:line48,line56代表了__getattr__的做用。line49代表obj.__dict__优先于Clz.__dict__

 

  前面提到过,类的也是对象,类是元类(metaclass)的实例,因此类属性的查找顺序基本同上,区别在于第二步,因为Clz可能有基类,因此是在Clz及其基类的__dict__查找“attr"

  

  文末,咱们再来看一下这段代码。

   

复制代码

 1 import functools, time 2 class cached_property(object): 3     """ A property that is only computed once per instance and then replaces 4         itself with an ordinary attribute. Deleting the attribute resets the 5         property. """ 6  7     def __init__(self, func): 8         functools.update_wrapper(self, func) 9         self.func = func10 11     def __get__(self, obj, cls):12         if obj is None: return self13         value = obj.__dict__[self.func.__name__] = self.func(obj)14         return value15 16 class TestClz(object):17     @cached_property18     def complex_calc(self):19         print 'very complex_calc'20         return sum(range(100))21 22 if __name__=='__main__':23     t = TestClz()24     print '>>> first call'25     print t.complex_calc26     print '>>> second call'27     print t.complex_calc

复制代码

 

    cached_property是一个non-data descriptor。在TestClz中,用cached_property装饰方法complex_calc,返回值是一个descriptor实例,因此在调用的时候没有使用小括号。

    第一次调用t.complex_calc以前,obj(t)的__dict__中没有”complex_calc“, 根据查找顺序第三条,执行cached_property.__get__, 这个函数代用缓存的complex_calc函数计算出结果,而且把结果放入obj.__dict__。那么第二次访问t.complex_calc的时候,根据查找顺序,第二条有限于第三条,因此就直接返回obj.__dict__['complex_calc']。bottle的源码中还有两个descriptor,很是厉害!