Python 的描述符 descriptor詳解
Python 在 2.2 版本中引入了descriptor(描述符)功能,也正是基于這個(gè)功能實(shí)現(xiàn)了新式類(new-styel class)的對(duì)象模型,同時(shí)解決了之前版本中經(jīng)典類 (classic class) 系統(tǒng)中出現(xiàn)的多重繼承中的 MRO(Method Resolution Order) 問題,另外還引入了一些新的概念,比如 classmethod, staticmethod, super, Property 等。因此理解 descriptor 有助于更好地了解 Python 的運(yùn)行機(jī)制。
那么什么是 descriptor 呢?
簡(jiǎn)而言之:descriptor 就是一類實(shí)現(xiàn)了__get__(), __set__(), __delete__()方法的對(duì)象。
Orz...如果你瞬間頓悟了,那么請(qǐng)收下我的膝蓋;
O_o!...如果似懂非懂,那么恭喜你!說明你潛力很大,咱們可以繼續(xù)挖掘:
引言
對(duì)于陌生的事物,一個(gè)具體的栗子是最好的學(xué)習(xí)方式,首先來看這樣一個(gè)問題:假設(shè)我們給一次數(shù)學(xué)考試創(chuàng)建一個(gè)類,用于記錄每個(gè)學(xué)生的學(xué)號(hào)、數(shù)學(xué)成績(jī)、以及提供一個(gè)用于判斷是否通過考試的check 函數(shù):
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
self.score = score
def check(self):
if self.score >= 60:
return 'pass'
else:
return 'failed'
很簡(jiǎn)單一個(gè)示例,看起來運(yùn)行的不錯(cuò):
xiaoming = MathScore(10, 90) xiaoming.score Out[3]: 90 xiaoming.std_id Out[4]: 10 xiaoming.check() Out[5]: 'pass'
但是會(huì)有一個(gè)問題,比如手一抖錄入了一個(gè)負(fù)分?jǐn)?shù),那么他就得悲劇的掛了:
xiaoming = MathScore(10, -90) xiaoming.score Out[8]: -90 xiaoming.check() Out[9]: 'failed'
這顯然是一個(gè)嚴(yán)重的問題,怎么能讓一個(gè)數(shù)學(xué) 90+ 的孩子掛科呢,于是乎一個(gè)簡(jiǎn)單粗暴的方法就誕生了:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.score = score
def check(self):
if self.score >= 60:
return 'pass'
else:
return 'failed'
上面再類的初始化函數(shù)中增加了負(fù)數(shù)判斷,雖然不夠優(yōu)雅,甚至有點(diǎn)拙劣,但這在實(shí)例初始化時(shí)確實(shí)工作的不錯(cuò):
xiaoming = MathScore(10, -90)
Traceback (most recent call last):
File "<ipython-input-12-6faad631790d>", line 1, in <module>
xiaoming = MathScore(10, -90)
File "C:/Users/xu_zh/.spyder2-py3/temp.py", line 14, in __init__
raise ValueError("Score can't be negative number!")
ValueError: Score can't be negative number!
OK, 但我們還無法阻止實(shí)例對(duì) score 的賦值操作,畢竟修改成績(jī)也是常有的事:
xiaoming = MathScore(10, 90) xiaoming = -10 # 無法判斷出錯(cuò)誤
對(duì)于大多數(shù)童鞋,這個(gè)問題 so easy 的啦:將 score 變?yōu)樗接?,從而禁?xiaoming.score 這樣的直接調(diào)用,增加一個(gè) get_score 和 set_score 用于讀寫:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
def get_score(self):
return self.__score
def set_score(self, value):
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
這確實(shí)是種常見的解決方法,但是不得不說這簡(jiǎn)直丑爆了:
調(diào)用成績(jī)?cè)僖膊荒苁褂?xiaoming.score 這樣自然的方式,需要使用 xiaoming.get_score() ,這看起來像口吃在說話!
還有那反人類的下劃線和括號(hào)...那應(yīng)該只出現(xiàn)在計(jì)算機(jī)之間竊竊私語之中...
賦值也無法使用 xiaoming.score = 80, 而需使用 xiaoming.set_score(80), 這對(duì)數(shù)學(xué)老師來說,太 TM 不自然了 !!!
作為一門簡(jiǎn)潔優(yōu)雅的編程語言,Python 是不會(huì)坐視不管的,于是其給出了 Property 類:
Property 類
先不管 Property 是啥,咱先看看它是如何簡(jiǎn)潔優(yōu)雅的解決上面這個(gè)問題的:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
def __get_score__(self):
return self.__score
def __set_score__(self, value):
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
score = property(__get_score__, __set_score__)
與上段代碼相比,主要是在最后一句實(shí)例化了一個(gè) property 實(shí)例,并取名為 score, 這個(gè)時(shí)候,我們就能如此自然的對(duì) instance.__score 進(jìn)行讀寫了:
xiaoming = MathScore(10, 90)
xiaoming.score
Out[30]: 90
xiaoming.score = 80
xiaoming.score
Out[32]: 80
xiaoming.score = -90
Traceback (most recent call last):
File "<ipython-input-33-aed7397ed552>", line 1, in <module>
xiaoming.score = -90
File "C:/Users/xu_zh/.spyder2-py3/temp.py", line 28, in __set_score__
raise ValueError("Score can't be negative number!")
ValueError: Score can't be negative number!
WOW~~一切工作正常!
嗯,那么問題來了:它是怎么工作的呢?
先看下 property 的參數(shù):
class property(fget=None, fset=None, fdel=None, doc=None) #拷貝自 Python 官方文檔
它的工作方式:
實(shí)例化 property 實(shí)例(我知道這是句廢話);
調(diào)用 property 實(shí)例(比如xiaoming.score)會(huì)直接調(diào)用 fget,并由 fget 返回相應(yīng)值;
對(duì) property 實(shí)例進(jìn)行賦值操作(xiaoming.score = 80)則會(huì)調(diào)用 fset,并由 fset 定義完成相應(yīng)操作;
刪除 property 實(shí)例(del xiaoming),則會(huì)調(diào)用 fdel 實(shí)現(xiàn)該實(shí)例的刪除;
doc 則是該 property 實(shí)例的字符說明;
fget/fset/fdel/doc 需自定義,如果只設(shè)置了fget,則該實(shí)例為只讀對(duì)象;
這看起來和本篇開頭所說的 descriptor 的功能非常相似,讓我們回顧一下 descriptor:
“descriptor 就是一類實(shí)現(xiàn)了__get__(), __set__(), __delete__()方法的對(duì)象。”
@~@ 如果你這次又秒懂了,那么請(qǐng)?jiān)俅问障挛业南ドw Orz...
另外,Property 還有個(gè)裝飾器語法糖 @property,其所實(shí)現(xiàn)的功能與 property() 完全一樣:
class MathScore():
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
@property
def score(self):
return self.__score
@score.setter
def score(self, value): #注意方法名稱要與上面一致,否則會(huì)失效
if value < 0:
raise ValueError("Score can't be negative number!")
self.__score = value
我們知道了 property 實(shí)例的工作方式了,那么問題又來了:它是怎么實(shí)現(xiàn)的?
事實(shí)上 Property 確實(shí)是基于 descriptor 而實(shí)現(xiàn)的,下面進(jìn)入我們的正題 descriptor 吧!
descriptor 描述符
照樣先不管 descriptor 是啥,咱們還是先看栗子,對(duì)于上面 Property 實(shí)現(xiàn)的功能,我們可以通過自定義的 descriptor 來實(shí)現(xiàn):
class NonNegative():
def __init__(self):
pass
def __get__(self, ist, cls):
return 'descriptor get: ' + str(ist.__score ) #這里加上字符描述便于看清調(diào)用
def __set__(self, ist, value):
if value < 0:
raise ValueError("Score can't be negative number!")
print('descriptor set:', value)
ist.__score = value
class MathScore():
score = NonNegative()
def __init__(self, std_id, score):
self.std_id = std_id
if score < 0:
raise ValueError("Score can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
我們新定義了一個(gè) NonNegative 類,并在其內(nèi)實(shí)現(xiàn)了__get__、__set__方法,然后在 MathScore 類中實(shí)例化了一個(gè) NonNegative 的實(shí)例 score,注意?。。≈匾氖虑檎f三遍:score 實(shí)例是 MathScore 的類屬性?。?!類屬性?。?!類屬性?。?!這個(gè) Mathscore.score 屬性同上面 Property 的 score 實(shí)例的功能是一樣的,只不過 Mathscore.score 調(diào)用的 get、set 并不定義在 Mathscore 內(nèi),而是定義在 NonNegative 類中,而 NonNegative 類就是一個(gè) descriptor 對(duì)象!
納尼? NonNegative 類的定義中可沒見到半個(gè) “descriptor” 的字樣,怎么就成了 descriptor 對(duì)象???
淡定! 重要的事情這里只說一遍:任何實(shí)現(xiàn) __get__,__set__ 或 __delete__ 方法中一至多個(gè)的類,就是 descriptor 對(duì)象。所以 NonNegative 自然是一個(gè) descriptor 對(duì)象。
那么 descriptor 對(duì)象與普通類比有什么特別之處呢? 先不急,來看看上端代碼的效果:
xiaoming = MathScore(10, 90) xiaoming.score Out[67]: 'descriptor get: 90' xiaoming.score = 80 descriptor set: 80 wangerma = MathScore(11, 70) wangerma.score Out[70]: 'descriptor get: 70' wangerma.score = 60 Out[70]: descriptor set: 60 wangerma.score Out[73]: 'descriptor get: 60' xiaoming.score Out[74]: 'descriptor get: 80' xiaoming.score = -90 ValueError: Score can't be negative number!
可以發(fā)現(xiàn),MathScore.score 雖然是一個(gè)類屬性,但它卻可以通過實(shí)例的進(jìn)行賦值,且面對(duì)不同的 MathScore 實(shí)例 xiaoming、wangerma 的賦值和調(diào)用,并不會(huì)產(chǎn)生沖突!因此看起來似乎更類似于 MathScore 的實(shí)例屬性,但與實(shí)例屬性不同的是它并不通過 MathScore 實(shí)例的讀寫方法操作值,而總是通過 NonNegative 實(shí)例的 __get__ 和 __set__ 對(duì)值進(jìn)行操作,那么它是怎么做到這點(diǎn)的?
注意看 __get__、__set__ 的參數(shù)
def __get__(self, ist, cls): #self:descriptor 實(shí)例本身(如 Math.score),ist:調(diào)用 score 的實(shí)例(如 xiaoming),cls:descriptor 實(shí)例所在的類(如MathScore)
...
def __set__(self, ist, value): #score 就是通過這些傳入的 ist 、cls 參數(shù),實(shí)現(xiàn)對(duì) MathScore 及其具體實(shí)例屬性的調(diào)用和改寫的
...
OK, 現(xiàn)在我們基本搞清了 descriptor 實(shí)例是如何實(shí)現(xiàn)對(duì)宿主類的實(shí)例屬性進(jìn)行模擬的。事實(shí)上 Property 實(shí)例的實(shí)現(xiàn)方式與上面的 NonNegative 實(shí)例類似。那么我們既然有了 Propery,為什么還要去自定義 descriptor 呢?
答案在于:更加逼真的模擬實(shí)例屬性(想想 MathScore.__init__里面那惡心的判斷語句),還有最重要的是:代碼重用?。?!
簡(jiǎn)而言之:通過單個(gè) descriptor 對(duì)象,可以更加逼真的模擬實(shí)例屬性,并且可以實(shí)現(xiàn)對(duì)宿主類實(shí)例的多個(gè)實(shí)例屬性進(jìn)行操作。
O.O! 如果你又秒懂了,那么你可以直接跳到下面寫評(píng)論了...
看個(gè)栗子:假如不僅要判斷學(xué)生的分?jǐn)?shù)是否為負(fù)數(shù),而且還要判學(xué)生的學(xué)號(hào)是否為負(fù)值,使用 property 的實(shí)現(xiàn)方式是這樣子的:
class MathScore():
def __init__(self, std_id, score):
if std_id < 0:
raise ValueError("Can't be negative number!")
self.__std_id = std_id
if score < 0:
raise ValueError("Can't be negative number!")
self.__score = score
def check(self):
if self.__score >= 60:
return 'pass'
else:
return 'failed'
@property
def score(self):
return self.__score
@score.setter
def score(self, value):
if value < 0:
raise ValueError("Can't be negative number!")
self.__score = value
@property
def std_id(self):
return self.__std_id
@std_id.setter
def std_id(self, idnum):
if idnum < 0:
raise ValueError("Can't be negative nmuber!")
self.__std_id = idnum
Property 實(shí)例最大的問題是:
無法影響宿主類實(shí)例的初始化,所以咱必須在__init__ 加上那丑惡的 if ...
單個(gè) Property 實(shí)例僅能針對(duì)宿主類實(shí)例的單個(gè)屬性,如果需要對(duì)多個(gè)屬性進(jìn)行控制,則必須定義多個(gè) Property 實(shí)例, 這真是太蛋疼了!
但是自定義 descriptor 可以很好的解決這個(gè)問題,看下實(shí)現(xiàn):
class NonNegative():
def __init__(self):
self.dic = dict()
def __get__(self, ist, cls):
print('Description get', ist)
return self.dic[ist]
def __set__(self, ist, value):
print('Description set', ist, value)
if value < 0:
raise ValueError("Can't be negative number!")
self.dic[ist] = value
class MathScore():
score = NonNegative()
std_id = NonNegative()
def __init__(self, std_id, score):
#這里并未創(chuàng)建實(shí)例屬性 std_id 和 score, 而是調(diào)用 MathScore.std_id 和 MathScore.score
self.std_id = std_id
self.score = score
def check(self):
if self.score >= 60:
return 'pass'
else:
return 'failed'
哈哈~! MathScore.__init__ 內(nèi)終于沒了 if ,代碼也比上面的簡(jiǎn)潔不少,但是功能一個(gè)不少,且實(shí)例之間不會(huì)相互影響:
事實(shí)上,MathScore 多個(gè)實(shí)例的同一個(gè)屬性,都是通過單個(gè) MathScore 類的相應(yīng)類屬性(也即 NonNegative 實(shí)例)操作的,這同 property 一致,但它又是怎么克服 Property 的兩個(gè)不足的呢?秘訣有三個(gè):
Property 實(shí)例本質(zhì)上是借助類屬性,變向?qū)?shí)例屬性進(jìn)行操作,而 NonNegative 實(shí)例則是完全通過類屬性模擬實(shí)例屬性,因此實(shí)例屬性其實(shí)根本不存在;
NonNegative 實(shí)例使用字典記錄每個(gè) MathScore 實(shí)例及其對(duì)應(yīng)的屬性值,其中 key 為 MathScore 實(shí)例名:比如 score 實(shí)例就是使用 dic = {‘Zhangsan':50, ‘Lisi':90} 記錄每個(gè)實(shí)例對(duì)應(yīng)的 score 值,從而確保可以實(shí)現(xiàn)對(duì) MathScore 實(shí)例屬性的模擬;
MathScore 通過在__init__內(nèi)直接調(diào)用類屬性,從而實(shí)現(xiàn)對(duì)實(shí)例屬性初始化賦值的模擬,而 Property 則不可能,因?yàn)?Property 實(shí)例(也即MathScore的類屬性)是真實(shí)的操作 MathScore 實(shí)例傳入的實(shí)例屬性以達(dá)到目的,但如果在初始化程序中傳入的不是實(shí)例屬性,而是類屬性(也即 Property 實(shí)例本身),則會(huì)陷入無限遞歸(PS:想一下如果將前一個(gè)property 實(shí)例實(shí)現(xiàn)中的self.__score 改成這里的 self.score 會(huì)發(fā)生什么)。
這三點(diǎn)看的似懂非懂,沒關(guān)系,來個(gè)比喻:
每個(gè) descriptor 實(shí)例(MathScore.score 和 MathScore.std_id)都是類作用域里的一個(gè)籃子,籃子里放著寫著每個(gè) MathScore 實(shí)例名字的盒子(‘zhangsan','lisi‘),同一個(gè)籃子里的盒子只記錄同樣屬性的值(比如score籃子里的盒子只記錄分?jǐn)?shù)值),當(dāng) MathScore 的實(shí)例對(duì)相應(yīng)屬性進(jìn)行操作時(shí),則找到對(duì)應(yīng)的籃子,取出標(biāo)有該實(shí)例名字的盒子,并對(duì)其進(jìn)行操作。
因此,實(shí)例對(duì)應(yīng)的屬性,壓根不在實(shí)例自己的作用域內(nèi),而是在類作用域的籃子里,只不過我們可以通過 xiaoming.score 這樣的方式進(jìn)行操作而已,所以其實(shí)際的調(diào)用的邏輯是這樣的:下圖右側(cè)的實(shí)例分別通過紅線和黑線對(duì)score和std_id 進(jìn)行操作,他們首先通過類調(diào)用相應(yīng)的類屬性,然后類屬性通過對(duì)應(yīng)的 descriptor 實(shí)例作用域?qū)Σ僮鬟M(jìn)行處理,并返回給類屬性相應(yīng)結(jié)果,最后讓實(shí)例感知到。
看到這里,很多童鞋可能不淡定了,因?yàn)榇蠹叶贾涝?Python 中采取 xiaoming.score = 10 這樣的賦值方式,如果 xiaoming 沒有 score 這樣的實(shí)例屬性,必定會(huì)自動(dòng)創(chuàng)建該實(shí)例屬性,怎么會(huì)去調(diào)用 MathScore 的 score 呢?
首先,要鼓掌?。?! 給想到這點(diǎn)的童鞋點(diǎn)贊?。?!其實(shí)上面在說 Property 的時(shí)候這個(gè)問題就產(chǎn)生了。
其次,Python 為了實(shí)現(xiàn) discriptor 確實(shí)對(duì)屬性的調(diào)用順序做出了相應(yīng)的調(diào)整,這些將會(huì)“Python 的 descriptor(下)”中介紹。
相關(guān)文章
基于PyQT5制作一個(gè)課堂點(diǎn)名系統(tǒng)
這篇文章主要為大家介紹一個(gè)基于PyQt5實(shí)現(xiàn)的抖音同款課堂點(diǎn)名系統(tǒng),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起動(dòng)手試一試2022-02-02
django學(xué)習(xí)之a(chǎn)jax post傳參的2種格式實(shí)例
AJAX除了異步的特點(diǎn)外,還有一個(gè)就是:瀏覽器頁面局部刷新,下面這篇文章主要給大家介紹了關(guān)于django學(xué)習(xí)之a(chǎn)jax post傳參的2種格式的相關(guān)資料,需要的朋友可以參考下2021-05-05
Django之第三方平臺(tái)QQ授權(quán)登錄的實(shí)現(xiàn)
本文主要介紹了Django之第三方平臺(tái)QQ授權(quán)登錄的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
Python實(shí)現(xiàn)生成簡(jiǎn)單的Makefile文件代碼示例
這篇文章主要介紹了Python實(shí)現(xiàn)生成簡(jiǎn)單的Makefile文件代碼示例,本文給出了兩段實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-03-03
python編程webpy框架模板之def with學(xué)習(xí)
這篇文章主要為大家介紹了python編程web.py框架模板之def with的學(xué)習(xí)有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11
Python字符串處理函數(shù)簡(jiǎn)明總結(jié)
這篇文章主要介紹了Python字符串處理函數(shù)簡(jiǎn)明總結(jié),本文總結(jié)了一些常用的字符串處理函數(shù),需要的朋友可以參考下2015-04-04
淺談Pandas:Series和DataFrame間的算術(shù)元素
今天小編就為大家分享一篇淺談Pandas:Series和DataFrame間的算術(shù)元素,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-12-12
python+openCV調(diào)用攝像頭拍攝和處理圖片的實(shí)現(xiàn)
這篇文章主要介紹了python+openCV調(diào)用攝像頭拍攝和處理圖片的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08

