標籤:python
在Python中,屬性尋找(attribute lookup)是比較複雜的,特別是涉及到描述符descriptor的時候。
在上一文章末尾,給出了一段代碼,就涉及到descriptor與attribute lookup的問題。而get系列函數(__get__, __getattr__, __getattribute__) 也很容易搞暈,本文就這些問題簡單總結一下。
首先,我們知道:
按照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
下面是測試代碼:
650) this.width=650;" id="code_img_closed_d0bbfca4-224e-4463-8168-7a098b38f1e2" class="code_img_closed" src="/img/jia.gif" style="margin:0px;padding:0px 5px 0px 0px;border:0px;vertical-align:middle;" /> 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"
文末,我們再來看一下這段代碼。
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />
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
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin:0px;padding:0px;border:none;" />
cached_property是一個non-data descriptor。在TestClz中,用cached_property裝飾方法complex_calc,傳回值是一個descriptor執行個體,所以在調用的時候沒有使用小括弧。
第一次調用t.complex_calc之前,obj(t)的__dict__中沒有”complex_calc“, 根據尋找順序第三條,執行cached_property.__get__, 這個函數代用緩衝的complex_calcFunction Compute出結果,並且把結果放入obj.__dict__。那麼第二次訪問t.complex_calc的時候,根據尋找順序,第二條有限於第三條,所以就直接返回obj.__dict__[‘complex_calc‘]。bottle的源碼中還有兩個descriptor,非常厲害!
python屬性尋找 深入理解(attribute lookup)