1. 程式人生 > >python屬性訪問順序 --描述符、__getattr__()

python屬性訪問順序 --描述符、__getattr__()

在Python中,訪問一個屬性的優先順序順序按照如下順序:
1、_getattribute_()
2.類屬性
3.資料描述符
4.例項屬性
5.非資料描述符
6.__getattr__()方法

需要注意的是:類屬性和例項屬性屬於不同的屬性集,因此這裡類屬性排序並不嚴謹。

首先來說下什麼是描述符。

官方的定義:描述符是一種具有“捆綁行為”的物件屬性。訪問(獲取、設定和刪除)它的屬性時,實際是呼叫特殊的方法(get(),set(),delete())。

通常情況下,訪問一個物件的搜尋鏈是怎樣的?比如a.x,首先,應該是查詢 a.dict[‘x’],然後是type(a).dict

[‘x’],一直向上知道元類那層止(不包括元類)。如果這個屬性是一個描述符呢?那python就會“攔截”這個搜尋鏈,取而代之呼叫描述符方法(get)。

三個方法(協議):
_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)把一個描述符作為某個物件的屬性。這個屬性要更改,比如增加判斷,或變得更復雜的時候,所有的處理只要在描述符中操作就行了。