python屬性訪問順序 --描述符、__getattr__()
在Python中,訪問一個屬性的優先順序順序按照如下順序:
1、_getattribute_()
2.類屬性
3.資料描述符
4.例項屬性
5.非資料描述符
6.__getattr__()
方法
需要注意的是:類屬性和例項屬性屬於不同的屬性集,因此這裡類屬性排序並不嚴謹。
首先來說下什麼是描述符。
官方的定義:描述符是一種具有“捆綁行為”的物件屬性。訪問(獲取、設定和刪除)它的屬性時,實際是呼叫特殊的方法(get(),set(),delete())。
通常情況下,訪問一個物件的搜尋鏈是怎樣的?比如a.x,首先,應該是查詢 a.dict[‘x’],然後是type(a).dict
三個方法(協議):
• _get_(self, instance, owner)
—獲取屬性時呼叫,返回設定的屬性值,通常是_set_中的value,或者附加的其他組合值。
• _set_(self, instance, value)
— 設定屬性時呼叫,返回None.
• _delete_(self, instance)
— 刪除屬性時呼叫,返回None
其中,instance是這個描述符屬性所在的類的實體,而owner是描述符所在的類。
資料描述符(data descriptor)和非資料描述符(non-data descriptors):
資料描述符:定義了_set_ 和_get_方法的物件
非資料描述符:只定義了_get_方法的物件。通常方法都是非資料描述符。比如後面會談到的staticmethod,classmethod等。
區別:
1、位於搜尋鏈上的順序。搜尋鏈(或者優先鏈)的順序大概是這樣的:資料描述符>實體屬性(儲存在實體的_dict_中)>非資料描述符。
這個順序初看起來挺晦澀。解釋如下:
獲取一個屬性的時候:
首先,看這個屬性是不是一個數據描述符,如果是,就直接執行描述符的_get_,並返回值。
其次,如果這個屬性不是資料描述符,那就按常規去從_dict_裡面取。
下面來看一個示例:
class NoDataDesc(object): def __get__(self, noinstance, owner): print("get:nodatadesc.attr") return noinstance.freq*2 class DataDesc(object): def __get__(self, instance, owner): print("get:datadesc.attr") return instance.freq*4 def __set__(self, instance, value): print("set:datadesc.attr") print(instance, value) # 輸出<__main__.Test object at 0x104400438> -1 (將test例項傳了進來,然後下面給test.__dict__新增屬性) # 當呼叫到資料描述符時(self.data_result = data), 順序:1、取instance.freq值(=3) 2、取value的值(=-1) 3、計算instance.doc_freq的值(-3) instance.doc_freq = instance.freq*value def __delete__(self, instance): print("del datadesc.attr") raise BaseException class Test(object): data_result = DataDesc() nodata_result = NoDataDesc() def __init__(self, freq, data, nodata): self.freq = freq self.data_result = data self.test_data = data self.nodata_result = nodata if __name__ == '__main__': test = Test(3, -1, -1) # self.data_result = data 呼叫DataDesc.__set__方法,設定instance.doc_freq的值 # 在DataDesc類中給instance(test例項)綁定了doc_freq print(test.doc_freq) # 輸出-3 # 檢視例項test print(test) # 輸出<__main__.Test object at 0x104400438> # 看下test的屬性集 print(test.__dict__) # 輸出{'test_data': -1, 'nodata_result': -1, 'doc_freq': -3, 'freq': 3} # 資料描述符, 優先與例項屬性集 print(test.data_result) # 輸出12 (呼叫DataDesc.__get__方法,列印instance.freq*4的值) # 非資料描述符, 落後於test屬性集, 因此取出_dict__中的nodata_result的值 print(test.nodata_result) # 輸出-1
如何檢測一個物件是不是描述符?
如果把問題換成——一個物件要滿足什麼條件,它才是描述符呢——那是不是回答就非常簡單了?
答:只要定義了(set,get,delete)方法中的任意一種或幾種,它就是個描述符。
那麼,繼續問:怎麼判斷一個物件定義了這三種方法呢?
立馬有人可能就會回答:你是不是傻啊?看一下不就看出來了。。。
問題是,你看不到的時候呢?python內建的staticmethod,classmethod怎麼看?
正確的回答應該是:看這個物件的_dict_。
要看物件的_dict_好辦,直接dir(物件)就行了。現在可以寫出檢測物件是不是描述符的方法了:
def has_descriptor_attrs(obj):
return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))
def is_descriptor(obj):
"""obj can be instance of descriptor or the descriptor class"""
return bool(has_descriptor_attrs(obj))
def has_data_descriptor_attrs(obj):
return set(['__set__', '__delete__']) & set(dir(obj))
def is_data_descriptor(obj):
return bool(has_data_descriptor_attrs(obj))
測試一下:
print is_descriptor(classmethod), is_data_descriptor(classmethod)
print is_descriptor(staticmethod), is_data_descriptor(staticmethod)
print is_data_descriptor(property)
輸出:
(True, False)
(True, False)
True
看來,特性(property)是資料描述符
描述符有什麼用和好處:
1)一般情況下不會用到,建議:先定基本的,以後真有需要再擴充套件。別貪玩。
2)可以在設定屬性時,做些檢測等方面的處理
3)快取?
4)設定屬性不能被刪除?那定義_delete_方法,並raise 異常。
5)還可以設定只讀屬性
6)把一個描述符作為某個物件的屬性。這個屬性要更改,比如增加判斷,或變得更復雜的時候,所有的處理只要在描述符中操作就行了。