Python描述符 (descriptor) 詳解
1、什么是描述符?
python描述符是一個“綁定行為”的對象屬性,在描述符協議中,它可以通過方法重寫屬性的訪問。這些方法有 __get__(), __set__(), 和__delete__()。如果這些方法中的任何一個被定義在一個對象中,這個對象就是一個描述符。
以上為官方定義,純粹為了裝逼使用,一般人看這些定義都有一種問候祖先的沖動!
沒關系,看完本文,你就會理解什么叫描述符了!
2、講解描述符前,先看一下屬性:__dict__ (每個對象均具備該屬性)
作用:字典類型,存放本對象的屬性,key(鍵)即為屬性名,value(值)即為屬性的值,形式為{attr_key : attr_value}
對象屬性的訪問順序:
①.實例屬性
②.類屬性
③.父類屬性
④.__getattr__()方法
以上順序,切記切記!
1 class Test(object): 2 cls_val = 1 3 def __init__(self): 4 self.ins_val = 10 5 6 7 >>> t=Test() 8 >>> Test.__dict__ 9 mappingproxy({'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}) 10 >>> t.__dict__ 11 {'ins_val': 10} 12 13 >>> type(x)==X 14 True 15 16 #更改實例t的屬性cls_val,只是新增了該屬性,並不影響類Test的屬性cls_val 17 >>> t.cls_val = 20 18 >>> t.__dict__ 19 {'ins_val': 10, 'cls_val': 20} 20 >>> Test.__dict__ 21 mappingproxy({'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}) 22 23 #更改了類Test的屬性cls_val的值,由於事先增加了實例t的cls_val屬性,因此不會改變實例的cls_val值(井水不犯河水) 24 >>> Test.cls_val = 30 25 >>> t.__dict__ 26 {'ins_val': 10, 'cls_val': 20} 27 >>> Test.__dict__ 28 mappingproxy({'__module__': '__main__', 'cls_val': 30, '__init__': <function Test.__init__ at 0x0000000002E35D08>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None})
從以上代碼可以看出,實例t的屬性並不包含cls_val,cls_val是屬於類Test的。
3、魔法方法:__get__(), __set__(), __delete__()
方法的原型為:
① __get__(self, instance, owner)
② __set__(self, instance, value)
③ __del__(self, instance)
那么以上的 self, instance owner到底指社么呢?莫急莫急,聽我慢慢道來!
首先我們先看一段代碼:
1 #代碼 1 2 3 class Desc(object): 4 5 def __get__(self, instance, owner): 6 print("__get__...") 7 print("self : \t\t", self) 8 print("instance : \t", instance) 9 print("owner : \t", owner) 10 print('='*40, "\n") 11 12 def __set__(self, instance, value): 13 print('__set__...') 14 print("self : \t\t", self) 15 print("instance : \t", instance) 16 print("value : \t", value) 17 print('='*40, "\n") 18 19 20 class TestDesc(object): 21 x = Desc() 22 23 #以下為測試代碼 24 t = TestDesc() 25 t.x 26 27 #以下為輸出信息: 28 29 __get__... 30 self : <__main__.Desc object at 0x0000000002B0B828> 31 instance : <__main__.TestDesc object at 0x0000000002B0BA20> 32 owner : <class '__main__.TestDesc'> 33 ========================================
可以看到,實例化類TestDesc后,調用對象t訪問其屬性x,會自動調用類Desc的 __get__方法,由輸出信息可以看出:
① self: Desc的實例對象,其實就是TestDesc的屬性x
② instance: TestDesc的實例對象,其實就是t
③ owner: 即誰擁有這些東西,當然是 TestDesc這個類,它是最高統治者,其他的一些都是包含在它的內部或者由它生出來的
到此,我可以揭開小小的謎底了,其實,Desc類就是是一個描述符(描述符是一個類哦),為啥呢?因為類Desc定義了方法 __get__, __set__.
所以,某個類,只要是內部定義了方法 __get__, __set__, __delete__ 中的一個或多個,就可以稱為描述符(^_^,簡單吧)
說到這里,我們的任務還遠遠沒有完成,還存在很多很多的疑點?
問題1. 為什么訪問 t.x的時候,會直接去調用描述符的 __get__() 方法呢?
答:t為實例,訪問t.x時,根據常規順序,
首先:訪問Owner的__getattribute__()方法(其實就是 TestDesc.__getattribute__()),訪問實例屬性,發現沒有,然后去訪問父類TestDesc,找到了!
其次:判斷屬性 x 為一個描述符,此時,它就會做一些變動了,將 TestDesc.x 轉化為 TestDesc.__dict__['x'].__get__(None, TestDesc) 來訪問
然后:進入類Desc的 __get__()方法,進行相應的操作
問題2. 從上面 代碼1 我們看到了,描述符的對象 x 其實是類 TestDesc 的類屬性,那么可不可以把它變成實例屬性呢?
答:我說了你不算,你說了也不算,解釋器說了算,看看解釋器怎么說的。
1 #代碼 2 2 3 class Desc(object): 4 def __init__(self, name): 5 self.name = name 6 7 def __get__(self, instance, owner): 8 print("__get__...") 9 print('name = ',self.name) 10 print('='*40, "\n") 11 12 class TestDesc(object): 13 x = Desc('x') 14 def __init__(self): 15 self.y = Desc('y') 16 17 #以下為測試代碼 18 t = TestDesc() 19 t.x 20 t.y 21 22 #以下為輸出結果: 23 __get__... 24 name = x 25 ========================================
咦,為啥沒打印 t.y 的信息呢?
因為沒有訪問 __get__() 方法啊,哈哈,那么為啥沒有訪問 __get__() 方法呢?(問題真多)
因為調用 t.y 時刻,首先會去調用TestDesc(即Owner)的 __getattribute__() 方法,該方法將 t.y 轉化為TestDesc.__dict__['y'].__get__(t, TestDesc), 但是呢,實際上 TestDesc 並沒有 y這個屬性,y 是屬於實例對象的,所以,只能忽略了。
問題3. 如果 類屬性的描述符對象 和 實例屬性描述符的對象 同名時,咋整?
答:還是讓解釋器來解釋一下吧。
1 #代碼 3 2 3 class Desc(object): 4 def __init__(self, name): 5 self.name = name 6 print("__init__(): name = ",self.name) 7 8 def __get__(self, instance, owner): 9 print("__get__() ...") 10 return self.name 11 12 def __set__(self, instance, value): 13 self.value = value 14 15 class TestDesc(object): 16 _x = Desc('x') 17 def __init__(self, x): 18 self._x = x 19 20 21 #以下為測試代碼 22 t = TestDesc(10) 23 t._x 24 25 #輸入結果 26 __init__(): name = x 27 __get__() ...
不對啊,按照慣例,t._x 會去調用 __getattribute__() 方法,然后找到了 實例t 的 _x 屬性就結束了,為啥還去調用了描述符的 __get__() 方法呢?
這就牽扯到了一個查找順序問題:當Python解釋器發現實例對象的字典中,有與描述符同名的屬性時,描述符優先,會覆蓋掉實例屬性。
不信?來看一下 字典 :
1 >>> t.__dict__ 2 {} 3 4 >>> TestDesc.__dict__ 5 mappingproxy({'__module__': '__main__', '_x': <__main__.Desc object at 0x0000000002B0BA20>, '__init__': <function TestDesc.__init__ at 0x0000000002BC59D8>, '__dict__': <attribute '__dict__' of 'TestDesc' objects>, '__weakref__': <attribute '__weakref__' of 'TestDesc' objects>, '__doc__': None})
怎么樣,沒騙你吧?我這人老好了,從來不騙人!
我們再將 代碼3 改進一下, 刪除 __set__() 方法試試看會發生什么情況?
1 #代碼 4 2 3 class Desc(object): 4 def __init__(self, name): 5 self.name = name 6 print("__init__(): name = ",self.name) 7 8 def __get__(self, instance, owner): 9 print("__get__() ...") 10 return self.name 11 12 class TestDesc(object): 13 _x = Desc('x') 14 def __init__(self, x): 15 self._x = x 16 17 18 #以下為測試代碼 19 t = TestDesc(10) 20 t._x 21 22 #以下為輸出: 23 __init__(): name = x
我屮艸芔茻,咋回事啊?怎么木有去 調用 __get__() 方法?
其實,還是 屬性 查找優先級惹的禍,只是定義一個 __get__() 方法,為非數據描述符,優先級低於實力屬性的!!
問題4. 什么是數據描述符,什么是非數據描述符?
答:一個類,如果只定義了 __get__() 方法,而沒有定義 __set__(), __delete__() 方法,則認為是非數據描述符; 反之,則成為數據描述符
問題5. 天天提屬性查詢優先級,就不能總結一下嗎?
答:好的好的,客官稍等!
① __getattribute__(), 無條件調用
② 數據描述符:由 ① 觸發調用 (若人為的重載了該 __getattribute__() 方法,可能會調職無法調用描述符)
③ 實例對象的字典(若與描述符對象同名,會被覆蓋哦)
④ 類的字典
⑤ 非數據描述符
⑥ 父類的字典
⑦ __getattr__() 方法