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

  在Python中,屬性查找(attribute lookup)是比較複雜的,特別是涉及到描述符descriptor的時候。

  在上一文章末尾,給出了一段代碼,就涉及到descriptor與attribute lookup的問題。而get系列函數(__get__, __getattr__, __getattribute__) 也很容易搞暈,本文就這些問題簡單總結一下。html

  首先,咱們知道:
  •     python中一切都是對象,「everything is object」,包括類,類的實例,數字,模塊
  •     任何object都是類(class or type)的實例(instance)
  •     若是一個descriptor只實現了__get__方法,咱們稱之爲non-data descriptor, 若是同時實現了__get__ __set__咱們稱之爲data descriptor。
 

實例屬性查找

  按照python doc,若是obj是某個類的實例,那麼obj.name(以及等價的getattr(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__方法, 不然python

    (2)若是「attr」出如今obj的__dict__中, 那麼直接返回 obj.__dict__['attr'], 不然緩存

    (3)若是「attr」出如今Clz或其基類的__dict__中app

        (3.1)若是attr是non-data descriptor,那麼調用其__get__方法, 不然ssh

        (3.2)返回 __dict__['attr']ide

    (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()
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__ui

cached_property例子

  咱們再來看看上一文章的這段代碼。spa

 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,很是厲害!
 

類屬性查找

  前面提到過,類的也是對象,類是元類(metaclass)的實例,因此類屬性的查找順序基本同上。區別在於第二步,因爲Clz可能有基類,因此是在Clz及其基類的__dict__」查找「attr,注意這裏的查找並非直接返回clz.__dict__['attr']。具體來講,這第二步分爲如下兩種狀況:

  (2.1)若是clz.__dict__['attr']是一個descriptor(無論是data descriptor仍是non-data descriptor),都調用其__get__方法

  (2.2)不然返回clz.__dict__['attr']

  這就解釋了一個頗有意思的問題:method與function的問題


>>> class Widget(object):
... def func(self):
... pass
...
>>> w = Widget()
>>> Widget.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Widget' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Widget' objects>, '__doc__': None, 'func': <function func at 0x7fdc7d0d1668>})
>>> w.__dict__
{}


>>> Widget.__dict__['func']
<function func at 0x7fdc7d0d1668>
>>> Widget.func
<unbound method Widget.func>
>>>

  Widget是一個之定義了一個func函數的類,func是類的屬性,這個也能夠經過Widget.__dict__、w.__dict__看到。Widget.__dict__['func']返回的是一個function,但Widget.func是一個unbound method,即Widget.func並不等同於Widget.__dict__['func'],按照前面的類屬性的訪問順序,咱們能夠懷疑,func是一個descriptor,這樣纔不會走到第2.2這種狀況。驗證以下:

>>> dir(Widget.__dict__['func'])
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

屬性賦值

  Python的屬性賦值(attribute assignment)也會受到descriptor(data descriptor)的影響,同時也會受到__setattr__函數的影響。固然Python中還有一個setattr,setattr(x, 'foobar', 123)等價於x.foobar = 123,兩者都叫attribute assignment。

  首先看看__setattr__:

object.__setattr__(self, name, value)
Called when an attribute assignment is attempted. This is called instead of the normal mechanism

   那什麼是normal mechanism,簡單來講就是x.__dict__['foobar'] = 123,無論'foobar'以前是不是x的屬性(固然賦值以後就必定是了)。可是若是‘’foobar‘’是類屬性,且是data descriptor,那麼回優先調用__set__。咱們來看一個例子:

 1 class MaxValDes(object):
 2     def __init__(self, attr, max_val):
 3         self.attr = attr
 4         self.max_val = max_val
 5 
 6     def __get__(self, instance, typ):
 7         return instance.__dict__[self.attr]
 8 
 9     def __set__(self, instance, value):
10         instance.__dict__[self.attr] = min(self.max_val, value)
11         print 'MaxValDes __set__', self.attr, instance.__dict__[self.attr]
12 
13 class Widget(object):
14     a = MaxValDes('a', 10)
15     def __init__(self):
16         self.a = 0
17 
18     # def __setattr__(self, name, value):
19     #     self.__dict__[name] = value
20     #     print 'Widget __setattr__', name, self.__dict__[name] 
21 
22 if __name__ == '__main__':
23     w0 = Widget()
24     w0.a = 123

  輸出以下:

MaxValDes __set__ a 0
MaxValDes __set__ a 10

  能夠看到,即便Widget的實例也有一個‘a’屬性,可是調用w.a的時候會調用類屬性‘a’(一個descriptor)的__set__方法。若是不註釋掉第18到第20行,輸出以下

Widget __setattr__ a 0
Widget __setattr__ a 123

  能夠看到,優先調用Widget 的__setattr__方法。所以:對於屬性賦值,obj = Clz(), 那麼obj.attr = var,按照這樣的順序:

  (1)若是Clz定義了__setattr__方法,那麼調用該方法,不然

  (2)若是「attr」是出如今Clz或其基類的__dict__中, 且attr是data descriptor, 那麼調用其__set__方法, 不然

  (3)等價調用obj.__dict__['attr'] = var 

 

references

(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
相關文章
相關標籤/搜索