Python高級編程——描述符Descriptor超詳細講解(上篇之屬性優先級)
送你小心心記得關注我哦!!
進入正文
全文摘要
描述符descriptor,是python語言的重要特性,上上一個系列文章專門講解python高級編程系列之裝飾器decorator,從英文上看起來它們還的確有那麼一點點像,不過它們確實也是有關係的,後面會講解到。本次系列文章將帶各位小夥伴深入詳解python描述符descriptor,鑑於我自己的文章風格,我會和前面的文章一樣,依然從它的誕生背景開始,到它的深入應用,由淺入深,逐層深入。
重新聲明,python高級編程——描述符descriptor系列文章一共分為上、中、下、補充篇,本文講解上篇,後續會陸續更新系列文章哦,請記得持續關注哦!文章偏長,閱讀全文約30min。
上篇請參考:
全文目錄
01 從python對象的__dict__屬性開始說起
1.1 類屬性or實例屬性
1.2 子類屬性or父類屬性
1.3 python對象的__dict__屬性
02 python屬性優先級的實驗驗證
2.1 簡單的實現代碼
03 python的“屬性攔截器”
3.1 __getattibute__魔術方法
3.2 __getattibute__方法的升級實現
3.3 __getattibute__方法的使用陷阱
3.4 __getattibute__方法實用總結
04 python高級編程——描述符descriptor(中篇)(預告)
python描述符詳解(上篇)
python的描述符descriptor,這是屬於python高級編程的一些概念和實現方法,可能有很多的小夥伴還並沒有用到過,甚至每聽說過,但是在Python的面試過程中有可能會出現,究竟什麼是python描述符,有什麼作用,使用有什麼意義,它的誕生背景是什麼,很少有文章專門介紹這一塊,有的文章介紹的太過粗淺,以至於看過之後依然不能夠理解描述符的本質。鑑於此,我尋思著出一期專門講解python描述符的系列文章,跟前面的python裝飾器系列文章一樣,因為涉及到的內容偏多,本文依然是分為上、中、下、補充篇四個系列部分進行講解,本文是
第一篇——上篇,介紹Python的對象的屬性訪問優先級與對象屬性的簡單控制(屬性攔截器)。01
從對象的__dict__屬性開始說起
聲明:本文所採用的面向對象設計並不是十分嚴格,所以很多可能覺得面向對象的設計不合理,本文僅僅藉助簡單的示例講清楚一些python的語言特性。
01 對象的__dict__屬性
1.1 類屬性or實例屬性?
我們都知道,在Python的面向對象裡面,他的靈活程度是比其他語言更加靈活的,為什麼這麼說呢?因為以下幾點:
(1)python面向對象中,類屬性,我既可以通過類名訪問,也可以通過實例訪問(這與C#不同,C#的靜態屬性只能夠通過類訪問,不能夠通過實例訪問)
(2)python中的類方法(@classmethod裝飾的方法),我也可以通過類名訪問,也可以通過實例訪問(這與C#中的靜態方法也不一樣)
我們可以通過一個例子來說明,代碼如下:
class Animal:
name='老虎'
def __init__(self,name,age):
self.name=name
self.age=age
def eat(self):
return '我需要吃東西!'
@classmethod
def sleep(cls):
return '我需要睡覺'
a=Animal('老虎',5) #構造對象
print(a.name)
print(a.age)
print(a.eat()) #實例訪問實例方法
print(a.sleep()) #實例也可以訪問類方法哦!
#----------------------------------
print(Animal.name)
print(Animal.sleep())
上面的運行結果為:
老虎
5
我需要吃東西!
我需要睡覺
老虎
我需要睡覺
細心的小夥伴應該發現了一個問題,在Animal中我定義了兩個屬性name,一個是類屬性、一個是實例屬性,這裡因為這二者的值都是“老虎”,我沒有辦法辨別,a.name 到底是輸出的類屬性還是實例屬性啊,如果我們改一下,改成如下:
a=Animal('獅子',5) #構造對象
print(a.name)
發現運行結果為:
獅子
得到的結論是,a.name 訪問的是實例屬性。那到底怎麼判斷呢?通過我的__dict__屬性去判斷。
01 對象的__dict__屬性
1.2 子類屬性or父類屬性?
先看一個例子,代碼如下:
class Animal:
name='老虎'
def __init__(self,name='老虎',age=5):
self.name=name
self.age=age
def eat(self):
return '我需要吃東西!'
@classmethod
def sleep(cls):
return '我需要睡覺'
class Dog(Animal):
#name_='小狗'
def __init__(self,age):
self.age=age
d=Dog(8)
print(d.name)
print(d.age)
運行結果為:
老虎 #說明子類繼承了父類的類屬性
8
這裡通過兩個簡單的例子,說明了
兩種關係關係一:類成員與實例成員的關係(包括屬性和方法這兩種成員)
關係二:父類和子類的關係
那這二者具體的關係到底是怎麼樣的呢?先告訴你答案吧!
對於關係一而言,實例是可以訪問類的成員的,但是類不能訪問實例成員;對於關係二而言,子類繼承父類的類成員和實例成員。
01 對象的__dict__屬性
1.3 python對象的__dict__屬性
上面的兩個例子所反映出的實際上是一個“屬性的優先級問題”,即什麼樣的屬性優先訪問。
Python一切皆對象,故而都有__dict__屬性,包括對象實例、函數、類本身。我們可以通過對象的__dict__查看,它返回的是一個字典對象,對於下面的代碼,
class Animal:
name='老虎'
def __init__(self,name='老虎',age=5):
self.name=name
self.age=age
self.weight=200
def eat(self):
self.height=100
return '我需要吃東西!'
@classmethod
def sleep(cls):
return '我需要睡覺'
class Dog(Animal):
def __init__(self,age):
self.age=age
a=Animal()
d=Dog(8)
print(Animal.__dict__.keys())
print(a.__dict__.keys())
print(Dog.__dict__.keys())
print(d.__dict__.keys())
運行結果為:
dict_keys(['__module__', 'name', '__init__', 'eat', 'sleep', '__dict__', '__weakref__', '__doc__'])
dict_keys(['name', 'age', 'weight'])
dict_keys(['__module__', '__init__', '__doc__'])
dict_keys(['age'])
由此可見,實例a最先直接訪問的屬性就只有name,age,weight,並不包含類屬性name,我們可以這麼,name、age、weight是屬於a的,但是類屬性name是不屬於a的。
那麼,對象對屬性的訪問到底遵循怎樣一個優先級呢?先告訴你答案!
①.實例屬性
②.類屬性
③.父類的類屬性
④.__getattr__()方法(訪問一個不存在的屬性的時候發生的行為)
注意:這裡的優先級只是最基本的,沒有涉及到任何的屬性設置和屬性控制,後面還會講到屬性的優先級。
02
屬性優先級的實驗驗證
02 屬性優先級的實驗驗證
2.1 優先級的代碼實現
根據上面的描述,我們來做一個實驗,
class Animal:
name='老虎'
def __init__(self):
pass
class Dog(Animal):
age=10
def __init__(self,height):
self.height=height
def __getattr__(self,propertyname):
if propertyname=='weight':
return 200
else:
raise AttributeError
d=Dog(150)
print(d.height)
print(d.age)
print(d.name)
print(d.weight)
運行結果為:
150 #第一優先級訪問,訪問實例屬性
10 #第二優先級訪問,訪問類屬性
老虎 #第三優先級訪問,訪問父類的類屬性
200 #第四優先級訪問,如果這裡改成d.sex,則會拋出異常總結:對象屬性的訪問優先級順序為:①.實例屬性
②.類屬性
③.父類的類屬性
④.__getattr__()方法
注意:這裡的優先級只是暫時的哦!!!後面還會講到屬性的控制與代理,情況還會更加複雜一點。
總結
對象屬性的訪問優先級順序為:(這只是暫時的哦)
①.實例屬性
②.類屬性
③.父類的類屬性
④.__getattr__()方法
03
python的“屬性攔截器”
概述
我們經常看見下面的一些魔術方法,比如__getattribute__、__getattr__、__setattr__、__delattr__,那麼他們每一個函數代表什麼意思呢,在什麼樣的使用場景下使用呢?他們到底有什麼樣的作用,使用這些方法有什麼意義?本系列文章都將一一討論,但是,限於篇幅,本文只講第一個
__getattribute__函數。03 python的屬性攔截器
3.1 __getattribute__魔術方法
當一個屬性被訪問的時候發生的行為,稱之為“屬性攔截器”。其實這是對屬性的一種高級控制,我們也常稱之為“為屬性綁定行為”。
Python中只要定義了繼承object的類,就默認存在屬性攔截器,只不過是攔截後沒有進行任何操作,而是直接返回。所以我們可以自己改寫__getattribute__方法來實現相關功能,比如查看權限、打印log日誌等
Python的屬性訪問方式很直觀,使用點屬性運算符。在新式類中,對對象屬性的訪問,都會調用特殊方法__getattribute__。__getattribute__允許我們在訪問對象屬性時自定義訪問行為,但是使用它特別要小心無限遞歸的問題。
通過一個簡單的例子來說明:
class Animal(object):
run = '我會跑'
def die(self):
return '我會死'
class Dog(Animal):
color='Blue'
def __init__(self, name,age):
self.name=name
self.age = age
# 重寫__getattribute__。需要注意的是重寫的方法中不能
# 使用對象的點運算符訪問屬性,否則使用點運算符訪問屬性時,
# 會再次調用__getattribute__。這樣就會陷入無限遞歸。
# 可以使用super()方法避免這個問題。
def __getattribute__(self, key):
print('調用了__getattribute__屬性')
return super(Dog, self).__getattribute__(key)
def sound(self):
return "汪汪汪"
dog=Dog('泰迪',4)
print(dog.name,end='\n------------------------------\n') #調用實例屬性
print(dog.age,end='\n------------------------------\n')
print(dog.color,end='\n------------------------------\n') #調用方法屬性
print(dog.run,end='\n------------------------------\n') #調用父類的類屬性
print(dog.sound(),end='\n------------------------------\n') #調用實例方法
print(dog.die(),end='\n------------------------------\n') #調用父類的方法
運行結果為:
調用了__getattribute__屬性
泰迪
------------------------------
調用了__getattribute__屬性
4
------------------------------
調用了__getattribute__屬性
Blue
------------------------------
調用了__getattribute__屬性
我會跑
------------------------------
調用了__getattribute__屬性
汪汪汪
------------------------------
調用了__getattribute__屬性
我會死
------------------------------
從上面的例子中,我們可以看出,不管是訪問對象的實例屬性、類屬性、父類的類屬性、對象方法、父類方法,總是需要
先過__getattribute__著一關的,即如果定義了__getattribute__魔法方法,他總是優先調用,要不怎麼稱之為“屬性攔截器”呢。03 python的屬性攔截器
3.2 __getattribute__方法的升級實現
事實上,關於__getattribute__的重寫,並不是一定要按照上面的格式,他可以有更加靈活的方式去實現的,上面的攔截方式太簡單了,不管我做什麼都是打印同樣一句話,這很沒特點,我要使得調用不同的屬性,顯示不同的信息該怎麼做呢?比如我也可以這樣,將上面的__getattribute__函數體重新定義如下:
def __getattribute__(self, key):
if key=='name':
print('name屬性被調用了')
return super(Dog, self).__getattribute__(key)
elif key=='age':
print('age屬性被調用了')
return super(Dog, self).__getattribute__(key)
elif key=='color':
print('color屬性被調用了')
return super(Dog, self).__getattribute__(key)
elif key=='run':
print('run屬性被調用了')
return super(Dog, self).__getattribute__(key)
elif key=='sound':
print('sound方法被調用了')
return super(Dog, self).__getattribute__(key)
elif key=='die':
print('die方法屬性被調用了')
return super(Dog, self).__getattribute__(key)
else:
print('調用了__getattribute__屬性')
return super(Dog, self).__getattribute__(key)
def sound(self):
return "汪汪汪"
dog=Dog('泰迪',4)
print(dog.name,end='\n------------------------------\n') #調用實例屬性
print(dog.age,end='\n------------------------------\n')
print(dog.color,end='\n------------------------------\n') #調用方法屬性
print(dog.run,end='\n------------------------------\n') #調用父類的類屬性
print(dog.sound(),end='\n------------------------------\n') #調用實例方法
print(dog.die(),end='\n------------------------------\n') #調用父類的方法
print(dog.height,end='\n------------------------------\n') #調用一個不存在的屬性
運行結果為:
name屬性被調用了
泰迪
------------------------------
age屬性被調用了
4
------------------------------
color屬性被調用了
Blue
------------------------------
run屬性被調用了
我會跑
------------------------------
sound方法被調用了
汪汪汪
------------------------------
die方法屬性被調用了
我會死
------------------------------
調用了__getattribute__屬性
Traceback (most recent call last):顯示沒有定義height屬性。
注意:沒一個條件分支一定要返回父類的__getattribute__方法哦,如果沒有返回,會發現雖然打印了相關的提示信息,但是並沒有返回屬性或方法的值,而是返回的一個None。
03 python的屬性攔截器
3.3 __getattribute__魔術方法的使用陷阱
def __getattribute__(self, key):
if key=='sound':
print('sound方法被調用了')
return self.sound()
else:
print('調用了__getattribute__屬性')
return super(Dog, self).__getattribute__(key)
def sound(self):
return "汪汪汪"
比如改成如上代碼:上面的代碼運行並不會出現預期的結果,即
sound方法被調用了
汪汪汪
為什麼呢?因為調用self.sound()方法的時候一定會先調用__getattribute__方法,然後__getattribute__裡面又調用sound方法,然後又調用__getattribute__方法。。。。。。如此遞歸循環下去,而且沒有退出機制,直到程序崩潰。
03 python的屬性攔截器
3.4 __getattribute__方法的實用總結
(1)一定要在每一個需要訪問的屬性裡面設置返回值,否則會返回None,一般有兩種做法:
即:return super(Dog, self).__getattribute__(key)這種形式或:return object.__getattribute__(self,key)
即返回父類的__getattribute__方法。
(2)不要再在__getattribute__方法的定義內部顯示使用self.成員 這種方法,這樣可能會造成無限遞歸,導致系統崩潰。
04
“描述符”下篇預告
下一篇預告:
下面篇文章的重點是python類中屬性訪問、修改、刪除的控制方法:
限於篇幅,關於python高級編程之描述符descriptor系列文章的第一篇預熱就到這裡,在本文還沒有真正涉及到python的描述符,按照我一貫的風格,這裡只是介紹python面向對象中屬性的訪問優先級,屬性的控制等基礎知識,關於python描述符,歡迎繼續關注哦!
有興趣的小夥伴持續關注哦!
關注轉發文章私信老師(學習)獲取!
閱讀更多 馬士兵尚學堂 的文章