python属性查找(attribute lookup)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python属性查找(attribute lookup)相关的知识,希望对你有一定的参考价值。
在Python中,属性查找(attribute lookup)是比较复杂的,特别是涉及到描述符descriptor的时候。
在上一文章末尾,给出了一段代码,就涉及到descriptor与attribute lookup的问题。而get系列函数(__get__, __getattr__, __getattribute__) 也很容易搞晕,本文就这些问题简单总结一下。
首先,我们知道:
- python中一切都是对象,“everything is object”,包括类,类的实例,数字,模块
- 任何object都是类(class or type)的实例(instance)
- 如果一个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.
obj = Clz(), 那么obj.attr 顺序如下:
(1)如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__get__方法, 否则
(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
下面是测试代码:
1 #coding=utf-8 2 class DataDescriptor(object): 3 def __init__(self, init_value): 4 self.value = init_value 5 6 def __get__(self, instance, typ): 7 return ‘DataDescriptor __get__‘ 8 9 def __set__(self, instance, value): 10 print (‘DataDescriptor __set__‘) 11 self.value = value 12 13 class NonDataDescriptor(object): 14 def __init__(self, init_value): 15 self.value = init_value 16 17 def __get__(self, instance, typ): 18 return(‘NonDataDescriptor __get__‘) 19 20 class Base(object): 21 dd_base = DataDescriptor(0) 22 ndd_base = NonDataDescriptor(0) 23 24 25 class Derive(Base): 26 dd_derive = DataDescriptor(0) 27 ndd_derive = NonDataDescriptor(0) 28 same_name_attr = ‘attr in class‘ 29 30 def __init__(self): 31 self.not_des_attr = ‘I am not descriptor attr‘ 32 self.same_name_attr = ‘attr in object‘ 33 34 def __getattr__(self, key): 35 return ‘__getattr__ with key %s‘ % key 36 37 def change_attr(self): 38 self.__dict__[‘dd_base‘] = ‘dd_base now in object dict ‘ 39 self.__dict__[‘ndd_derive‘] = ‘ndd_derive now in object dict ‘ 40 41 def main(): 42 b = Base() 43 d = Derive() 44 print ‘Derive object dict‘, d.__dict__ 45 assert d.dd_base == "DataDescriptor __get__" 46 assert d.ndd_derive == ‘NonDataDescriptor __get__‘ 47 assert d.not_des_attr == ‘I am not descriptor attr‘ 48 assert d.no_exists_key == ‘__getattr__ with key no_exists_key‘ 49 assert d.same_name_attr == ‘attr in object‘ 50 d.change_attr() 51 print ‘Derive object dict‘, d.__dict__ 52 assert d.dd_base != ‘dd_base now in object dict ‘ 53 assert d.ndd_derive == ‘ndd_derive now in object dict ‘ 54 55 try: 56 b.no_exists_key 57 except Exception, e: 58 assert isinstance(e, AttributeError) 59 60 if __name__ == ‘__main__‘: 61 main()
注意第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 = func 10 11 def __get__(self, obj, cls): 12 if obj is None: return self 13 value = obj.__dict__[self.func.__name__] = self.func(obj) 14 return value 15 16 class TestClz(object): 17 @cached_property 18 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_calc 26 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,非常厉害!
references:
(1)Descriptor HowTo Guide, https://docs.python.org/2/howto/descriptor.html#descriptor-protocol
(2)Object attribute lookup in Python, http://www.betterprogramming.com/object-attribute-lookup-in-python.html
(3)python __set__ __get__ 等解释, http://blog.csdn.net/huithe/article/details/7484606