屬性查找

python set get 解釋:

 class T(object):  
    name = 'name'  
    def hello(self):  
        print 'hello'  
t = T()  
使用dir(t)列出t的所有有效屬性:
 >>> dir(t)  
    ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__',  
     '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__',  
     '__repr__', '__setattr__', '__str__', '__weakref__', 'hello', 'name']  
屬性可以分為兩類,一類是Python自動產生的,如__class__,__hash__等,另一類是我們自定義的,如上面的hello,name。我們只關心自定義屬性。
類和實例對象(實際上,Python中一切都是對象,類是type的實例)都有__dict__屬性,里面存放它們的自定義屬性(對與類,里面還存放了別的東西)。
>>> t.__dict__  
    {}  
>>> T.__dict__  
    <dictproxy object at 0x00CD0FF0>  
>>> dict(T.__dict__)            #由于T.__dict__并沒有直接返回dict對象,這里進行轉換,以方便觀察其中的內容  
    {'__module__': '__main__', 'name': 'name',  
     'hello': <function hello at 0x00CC2470>,  
     '__dict__': <attribute '__dict__' of 'T' objects>,  
     '__weakref__': <attribute '__weakref__' of 'T' objects>, '__doc__': None}  
 >>>   
 有些內建類型,如list和string,它們沒有__dict__屬性,隨意沒辦法在它們上面附加自定義屬性。
到現在為止t.__dict__是一個空的字典,因為我們并沒有在t上自定義任何屬性,它的有效屬性hello和name都是從T得到的。T的__dict__中包含hello和name。當遇到t.name語句時,Python怎么找到t的name屬性呢?

首先,Python判斷name屬性是否是個自動產生的屬性,如果是自動產生的屬性,就按特別的方法找到這個屬性,當然,這里的name不是自動產生的屬性,而是我們自己定義的,Python于是到t的__dict__中尋找。還是沒找到。
接著,Python找到了t所屬的類T,搜索T.__dict__,期望找到name,很幸運,直接找到了,于是返回name的值:字符串‘name’。如果在T.__dict__中還沒有找到,Python會接著到T的父類(如果T有父類的話)的__dict__中繼續查找。

這不足以解決我們的困惑,因為事情遠沒有這么簡單,上面說的其實是個簡化的步驟。
繼續上面的例子,對于name屬性T.name和T.__dict__['name']是完全一樣的。
     >>> T.name  
    'name'  
    >>> T.__dict__['name']  
    'name'  
    >>>   
但是對于hello,情形就有些不同了
     >>> T.hello  
    <unbound method T.hello>  
    >>> T.__dict__['hello']  
    <function hello at 0x00CC2470>  
    >>>   
 可以發現,T.hello是個unbound method。而T.__dict__['hello']是個函數(不是方法)。

推斷:方法在類的__dict__中是以函數的形式存在的(方法的定義和函數的定義簡直一樣,除了要把第一個參數設為self)。那么T.hello得到的應該也是個函數啊,怎么成了unbound method了。

再看看從實例t中訪問hello
 >>> t.hello  
<bound method T.hello of <__main__.T object at 0x00CD0E50>>  
>>>  
 是一個bound method。

有意思,按照上面的查找策略,既然在T的__dict__中hello是個函數,那么T.hello和t.hello應該都是同一個函數才對。到底是怎么變成方法的,而且還分為unbound method和bound method。

關于unbound和bound到還好理解,我們不妨先作如下設想:方法是要從實例調用的嘛(指實例方法,classmethod和staticmethod后面講),如果從類中訪問,如T.hello,hello沒有和任何實例發生聯系,也就是沒綁定(unbound)到任何實例上,所以是個unbound,對t.hello的訪問方式,hello和t發生了聯系,因此是bound。

但從函數<function hello at 0x00CC2470>到方法<unbound method T.hello>的確讓人費解。 

一切的魔法都源自今天的主角:descriptor

查找屬性時,如obj.attr,如果Python發現這個屬性attr有個__get__方法,Python會調用attr的__get__方法,返回__get__方法的返回值,而不是返回attr(這一句話并不準確,我只是希望你能對descriptor有個初步的概念)。

Python中iterator(怎么扯到Iterator了?)是實現了iterator協議的對象,也就是說它實現了下面兩個方法__iter__和next()。類似的,descriptor也是實現了某些特定方法的對象。descriptor的特定方法是__get__,__set__和__delete__,其中__set__和__delete__方法是可選的。iterator必須依附某個對象而存在(由對象的__iter__方法返回),descriptor也必須依附對象,作為對象的一個屬性,它而不能單獨存在。還有一點,descriptor必須存在于類的__dict__中,這句話的意思是只有在類的__dict__中找到屬性,Python才會去看看它有沒有__get__等方法,對一個在實例的__dict__中找到的屬性,Python根本不理會它有沒有__get__等方法,直接返回屬性本身。descriptor到底是什么呢:簡單的說,descriptor是對象的一個屬性,只不過它存在于類的__dict__中并且有特殊方法__get__(可能還有__set__和__delete)而具有一點特別的功能,為了方便指代這樣的屬性,我們給它起了個名字叫descriptor屬性。

可能你還是不明白,下面開始用例子說明。

先定義這個類:
     class Descriptor(object):  
        def __get__(self, obj, type=None):  
                return 'get', self, obj, type  
        def __set__(self, obj, val):  
            print 'set', self, obj, val  
        def __delete__(self, obj):  
            print 'delete', self, obj  
 這里__set__和__delete__其實可以不出現,不過為了后面的說明,暫時把它們全寫上。

下面解釋一下三個方法的參數:

self當然不用說,指的是當前Descriptor的實例。obj值擁有屬性的對象。這應該不難理解,前面已經說了,descriptor是對象的稍微有點特殊的屬性,這里的obj就是擁有它的對象,要注意的是,如果是直接用類訪問descriptor(別嫌啰嗦,descriptor是個屬性,直接用類訪問descriptor就是直接用類訪問類的屬性),obj的值是None。type是obj的類型,剛才說過,如果直接通過類訪問descriptor,obj是None,此時type就是類本身。

三個方法的意義,假設T是一個類,t是它的一個實例,d是T的一個descriptor屬性(牛什么啊,不就是有個__get__方法嗎!),value是一個有效值:

讀取屬性時,如T.d,返回的是d.__get__(None, T)的結果,t.d返回的是d.__get__(t, T)的結果。

設置屬性時,t.d = value,實際上調用d.__set__(t, value),T.d = value,這是真正的賦值,T.d的值從此變成value。刪除屬性和設置屬性類似。

下面用例子說明,看看Python中執行是怎么樣的:

重新定義我們的類T和實例t
 class T(object):  
    d = Descriptor()  
t = T() 
 d是T的類屬性,作為Descriptor的實例,它有__get__等方法,顯然,d滿足了所有的條件,現在它就是一個descriptor!
 >>> t.d         #t.d,返回的實際是d.__get__(t, T)  
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)  
>>> T.d        #T.d,返回的實際是d.__get__(None, T),所以obj的位置為None  
('get', <__main__.Descriptor object at 0x00CD9450>, None, <class '__main__.T'>)  
>>> t.d = 'hello'   #在實例上對descriptor設置值。要注意的是,現在顯示不是返回值,而是__set__方法中  
                               print語句輸出的。  
set <__main__.Descriptor object at 0x00CD9450> <__main__.T object at 0x00CD0E50> hello  
>>> t.d         #可見,調用了Python調用了__set__方法,并沒有改變t.d的值  
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)  
>>> T.d = 'hello'   #沒有調用__set__方法  
>>> T.d                #確實改變了T.d的值  
'hello'  
>>> t.d               #t.d的值也變了,這可以理解,按我們上面說的屬性查找策略,t.d是從T.__dict__中得到的  
                              T.__dict__['d']的值是'hello',t.d當然也是'hello'  
'hello' 
 data descriptor和non-data descriptor

象上面的d,同時具有__get__和__set__方法,這樣的descriptor叫做data descriptor,如果只有__get__方法,則叫做non-data descriptor。容易想到,由于non-data descriptor沒有__set__方法,所以在通過實例對屬性賦值時,例如上面的t.d = 'hello',不會再調用__set__方法,會直接把t.d的值變成'hello'嗎?口說無憑,實例為證:
     class Descriptor(object):  
        def __get__(self, obj, type=None):  
                return 'get', self, obj, type  
    class T(object):  
           d = Descriptor()  
    t = T()  
 >>> t.d  
('get', <__main__.Descriptor object at 0x00CD9550>, <__main__.T object at 0x00CD9510>, <class '__main__.T'>)  
>>> t.d = 'hello'  
>>> t.d  
'hello'  
>>>  
 在實例上對non-data descriptor賦值隱藏了實例上的non-data descriptor!

 

是時候坦白真正詳細的屬性查找策略 了,對于obj.attr(注意:obj可以是一個類):

1.如果attr是一個Python自動產生的屬性,找到!(優先級非常高!)

2.查找obj.__class__.__dict__,如果attr存在并且是data descriptor,返回data descriptor的__get__方法的結果,如果沒有繼續在obj.__class__的父類以及祖先類中尋找data descriptor

3.在obj.__dict__中查找,這一步分兩種情況,第一種情況是obj是一個普通實例,找到就直接返回,找不到進行下一步。第二種情況是obj是一個類,依次在obj和它的父類、祖先類的__dict__中查找,如果找到一個descriptor就返回descriptor的__get__方法的結果,否則直接返回attr。如果沒有找到,進行下一步。

4.在obj.__class__.__dict__中查找,如果找到了一個descriptor(插一句:這里的descriptor一定是non-data descriptor,如果它是data descriptor,第二步就找到它了)descriptor的__get__方法的結果。如果找到一個普通屬性,直接返回屬性值。如果沒找到,進行下一步。

5.很不幸,Python終于受不了。在這一步,它raise AttributeError

 

利用這個,我們簡單分析一下上面為什么要強調descriptor要在類中才行。我們感興趣的查找步驟是2,3,4。第2步和第4步都是在類中查找。對于第3步,如果在普通實例中找到了,直接返回,沒有判斷它有沒有__get__()方法。

 

對屬性賦值時的查找策略 ,對于obj.attr = value

1.查找obj.__class__.__dict__,如果attr存在并且是一個data descriptor,調用attr的__set__方法,結束。如果不存在,會繼續到obj.__class__的父類和祖先類中查找,找到 data descriptor則調用其__set__方法。沒找到則進入下一步。

2.直接在obj.__dict__中加入obj.__dict__['attr'] = value

 

順便分析下為什么在實例上對non-data descriptor賦值隱藏了實例上的non-data descriptor。

接上面的non-data descriptor例子
     >>> t.__dict__  
    {'d': 'hello'}  
在t的__dict__里出現了d這個屬性。根據對屬性賦值的查找策略,第1步,確實在t.__class__.__dict__也就是T.__dict__中找到了屬性d,但它是一個non-data descriptor,不滿足data descriptor的要求,進入第2步,直接在t的__dict__屬性中加入了屬性和屬性值。當獲取t.d時,執行查找策略,第2步在T.__dict__中找到了d,但它是non-data descriptor,步滿足要求,進行第3步,在t的__dict__中找到了d,直接返回了它的值'hello'。

 

說了這么半天,還沒到函數和方法!

算了,明天在說吧

簡單提一下,所有的函數(方法)都有__get__方法,當它們在類的__dict__中是,它們就是non-data descriptor。

python descriptor 詳解:
正文
descriptor簡介

在python中,如果一個新式類定義了__get__, __set__, __delete__方法中的一個或者多個,那么稱之為descriptor。descriptor有分為data descriptor與non-data descriptor, descriptor通常用來改變默認的屬性訪問(attribute lookup),這部分會在下一遍文章中介紹。注意 ,descriptor的實例是一定是的屬性(class attribute)。

  這三個特殊的函數簽名是這樣的:

  object.__get__(self, instance, owner):return value

  object.__set__(self, instance, value):return None

  object.__delete__(self, instance): return None

  
  下面的代碼展示了簡單的用法:
# -*- coding: utf-8 -*-
class Des(object):
    def __init__(self, init_value):
        self.value = init_value
 
    def __get__(self, instance, typ):
        print('call __get__', instance, typ)
        return self.value
 
    def __set__(self, instance, value):
        print ('call __set__', instance, value)
        self.value = value
 
    def __delete__(self, instance):
        print ('call __delete__', instance)
 
class Widget(object):
    t = Des(1)
 
def main():
    w = Widget()
    print type(w.t)
    w.t = 1
    print w.t, Widget.t
    del w.t
 
if __name__=='__main__':
    main()

運行結果如下:

    ('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
    <type 'int'>

    ('call __set__', <__main__.Widget object at 0x02868570>, 1)

    ('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
    1 ('call __get__', None, <class '__main__.Widget'>)

    1

    ('call __delete__', <__main__.Widget object at 0x02868570>)

從輸出結果可以看到,對于這個三個特殊函數,形參instance是descriptor實例所在的類的實例(w), 而形參owner就是這個類(widget)
  w.t 等價于 Pro.__get__(t, w, Widget).而Widget.t 等價于 Pro.__get__(t, None, Widget)

descriptor注意事項:
需要注意的是, descriptor的實例一定是類的屬性,因此使用的時候需要自行區分實例。比如下面這個例子,我們需要保證以下屬性不超過一定的閾值。

class MaxValDes(object):
    def __init__(self, inti_val, max_val):
        self.value = inti_val
        self.max_val = max_val

    def __get__(self, instance, typ):
        return self.value

    def __set__(self, instance, value):
        self.value= min(self.max_val, value)

class Widget(object):
    a = MaxValDes(0, 10)

if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a
    w0.a = 123
    print 'after set w0',w0.a
    w1 = Widget()
    print 'inited w1', w1.a
代碼很簡單,我們通過MaxValDes這個descriptor來保證屬性的值不超過一定的范圍。運行結果如下:

    inited w0 0
    after set w0 10
    inited w1 10
可以看到,對w0.a的賦值符合預期,但是w1.a的值卻不是0,而是同w0.a一樣。這就是因為,a是類Widget的類屬性, Widget的實例并沒有'a'這個屬性,可以通過__dict__查看。

那么要怎么修改才符合預期呢,看下面的代碼:
class MaxValDes(object):
    def __init__(self, attr, max_val):
        self.attr = attr
        self.max_val = max_val

    def __get__(self, instance, typ):
        return instance.__dict__[self.attr]

    def __set__(self, instance, value):
        instance.__dict__[self.attr] = min(self.max_val, value)

class Widget(object):
    a = MaxValDes('a', 10)
    b = MaxValDes('b', 12)
    def __init__(self):
        self.a = 0
        self.b = 1

if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a, w0.b
    w0.a = 123
    w0.b = 123
    print 'after set w0',w0.a, w0.b

    w1 = Widget()
    print 'inited w1', w1.a, w1.b

運行結果如下:
    inited w0 0 1
    after set w0 10 12
    inited w0 0 1
可以看到,運行結果比較符合預期,w0、w1兩個實例互不干擾。上面的代碼中有兩點需要注意:

  第一:第7、10行都是通過instance.__dict__來取值、賦值,而不是調用getattr、setattr,否則會遞歸調用,死循環。

  第二:現在類和類的實例都擁有‘a’屬性,不過w0.a調用的是類屬性‘a',具體原因參見下一篇文章

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

python getattr 巧妙應用

9_JZL@{C$DDD29IAI%TT$FG.png

Python概念-上下文管理協議中的enterexit

QQ截圖20180613111132.png

對比Python中getattrgetattribute獲取屬性的用法

python魔法方法:getattr,setattr,getattribute

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容

  • Python 面向對象Python從設計之初就已經是一門面向對象的語言,正因為如此,在Python中創建一個類和對...
    順毛閱讀 4,234評論 4 16
  • 1.該花的錢要花,該享受的要享受,該捐助的要捐助;趁我們還能吃能動,不要舍不得。 2.不必對死后的事考慮太多,因為...
    白頭布衣閱讀 266評論 0 0
  • 三月,日光溫柔,微風正好。
    我如何能靜閱讀 257評論 0 2
  • 夏至未至,燥然心頭。 何以解憂?唯有畫畫。 每一顆水果在光影里閃耀著誘人的色澤,就像,就像……誘惑了夏娃的那顆禁果...
    清清summer閱讀 328評論 1 3
  • 第一次用Pad畫的,大家喜歡嗎?沒有Applepencile 純手指啊……求打賞pencile……
    九點半月光閱讀 312評論 5 5