Python 的 descriptor(上)

Python 在 2.2 版本中引入了descriptor(描述符)功能,也正是基於這個功能實現了新式類(new-styel class)的對象模型,同時解決了以前版本中經典類 (classic class) 系統中出現的多重繼承中的 MRO(Method Resolution Order) 問題,另外還引入了一些新的概念,好比 classmethod, staticmethod, super, Property 等。所以理解 descriptor 有助於更好地瞭解 Python 的運行機制。html

那麼什麼是 descriptor 呢?python

簡而言之:descriptor 就是一類實現了__get__(), __set__(), __delete__()方法的對象。編程

Orz...若是你瞬間頓悟了,那麼請收下個人膝蓋;
O_o!...若是似懂非懂,那麼恭喜你!說明你潛力很大,我們能夠繼續挖掘:編程語言

引言

對於陌生的事物,一個具體的栗子是最好的學習方式,首先來看這樣一個問題:假設咱們給一次數學考試建立一個類,用於記錄每一個學生的學號、數學成績、以及提供一個用於判斷是否經過考試的check 函數:函數

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'

很簡單一個示例,看起來運行的不錯:學習

xiaoming = MathScore(10, 90)

xiaoming.score
Out[3]: 90

xiaoming.std_id
Out[4]: 10

xiaoming.check()
Out[5]: 'pass'

可是會有一個問題,好比手一抖錄入了一個負分數,那麼他就得悲劇的掛了:spa

xiaoming = MathScore(10, -90)

xiaoming.score
Out[8]: -90

xiaoming.check()
Out[9]: 'failed'

這顯然是一個嚴重的問題,怎麼能讓一個數學 90+ 的孩子掛科呢,因而乎一個簡單粗暴的方法就誕生了:.net

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'

上面再類的初始化函數中增長了負數判斷,雖然不夠優雅,甚至有點拙劣,但這在實例初始化時確實工做的不錯:code

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, 但咱們還沒法阻止實例對 score 的賦值操做,畢竟修改爲績也是常有的事:htm

xiaoming = MathScore(10, 90)

xiaoming = -10    # 沒法判斷出錯誤

對於大多數童鞋,這個問題 so easy 的啦:將 score 變爲私有,從而禁止 xiaoming.score 這樣的直接調用,增長一個 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

這確實是種常見的解決方法,可是不得不說這簡直醜爆了:

調用成績不再能使用 xiaoming.score 這樣天然的方式,須要使用 xiaoming.get_score() ,這看起來像口吃在說話!
還有那反人類的下劃線和括號...那應該只出如今計算機之間竊竊私語之中...
賦值也沒法使用 xiaoming.score = 80, 而需使用 xiaoming.set_score(80), 這對數學老師來講,太 TM 不天然了 !!!

做爲一門簡潔優雅的編程語言,Python 是不會坐視無論的,因而其給出了 Property 類:

Property 類

先無論 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'            
        
    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__)

與上段代碼相比,主要是在最後一句實例化了一個 property 實例,並取名爲 score, 這個時候,咱們就能如此天然的對 instance.__score 進行讀寫了:

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 的參數:

class property(fget=None, fset=None, fdel=None, doc=None)  #拷貝自 Python 官方文檔

它的工做方式:

  1. 實例化 property 實例(我知道這是句廢話);

  2. 調用 property 實例(好比xiaoming.score)會直接調用 fget,並由 fget 返回相應值;

  3. 對 property 實例進行賦值操做(xiaoming.score = 80)則會調用 fset,並由 fset 定義完成相應操做;

  4. 刪除 property 實例(del xiaoming),則會調用 fdel 實現該實例的刪除;

  5. doc 則是該 property 實例的字符說明;

  6. fget/fset/fdel/doc 需自定義,若是隻設置了fget,則該實例爲只讀對象;

這看起來和本篇開頭所說的 descriptor 的功能很是類似,讓咱們回顧一下 descriptor:

「descriptor 就是一類實現了__get__(), __set__(), __delete__()方法的對象。」

@~@ 若是你此次又秒懂了,那麼請再次收下個人膝蓋 Orz...

另外,Property 還有個裝飾器語法糖 @property,其所實現的功能與 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):    #注意方法名稱要與上面一致,不然會失效
        if value < 0:
            raise ValueError("Score can't be negative number!")
        self.__score = value

咱們知道了 property 實例的工做方式了,那麼問題又來了:它是怎麼實現的?
事實上 Property 確實是基於 descriptor 而實現的,下面進入咱們的正題 descriptor 吧!

descriptor 描述符

照樣先無論 descriptor 是啥,我們仍是先看栗子,對於上面 Property 實現的功能,咱們能夠經過自定義的 descriptor 來實現:

class NonNegative():
    
    def __init__(self):
        pass

    def __get__(self, ist, cls):
        return 'descriptor get: ' + str(ist._MathScore__score)  #這裏加上字符描述便於看清調用

    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'

咱們新定義了一個 NonNegative 類,並在其內實現了__get____set__方法,而後在 MathScore 類中實例化了一個 NonNegative 的實例 score,注意!!!重要的事情說三遍:score 實例是 MathScore 的類屬性!!!類屬性!!!類屬性!!!這個 Mathscore.score 屬性同上面 Property 的 score 實例的功能是同樣的,只不過 Mathscore.score 調用的 get、set 並不定義在 Mathscore 內,而是定義在 NonNegative 類中,而 NonNegative 類就是一個 descriptor 對象!

納尼? NonNegative 類的定義中可沒見到半個 「descriptor」 的字樣,怎麼就成了 descriptor 對象???

淡定! 重要的事情這裏只說一遍:任何實現 __get____set____delete__ 方法中一至多個的類,就是 descriptor 對象。因此 NonNegative 天然是一個 descriptor 對象。

那麼 descriptor 對象與普通類比有什麼特別之處呢? 先不急,來看看上端代碼的效果:

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!

能夠發現,MathScore.score 雖然是一個類屬性,但它卻能夠經過實例的進行賦值,且面對不一樣的 MathScore 實例 xiaoming、wangerma 的賦值和調用,並不會產生衝突!所以看起來彷佛更相似於 MathScore 的實例屬性,但與實例屬性不一樣的是它並不經過 MathScore 實例的讀寫方法操做值,而老是經過 NonNegative 實例的 __get____set__ 對值進行操做,那麼它是怎麼作到這點的?

注意看 __get____set__ 的參數

def __get__(self, ist, cls):  #self:descriptor 實例自己(如 Math.score),ist:調用 score 的實例(如 xiaoming),cls:descriptor 實例所在的類(如MathScore)
        ...

    def __set__(self, ist, value):  #score 就是經過這些傳入的 ist 、cls 參數,實現對 MathScore 及其具體實例屬性的調用和改寫的
        ...

OK, 如今咱們基本搞清了 descriptor 實例是如何實現對宿主類的實例屬性進行模擬的。事實上 Property 實例的實現方式與上面的 NonNegative 實例相似。那麼咱們既然有了 Propery,爲何還要去自定義 descriptor 呢?

答案在於:更加逼真的模擬實例屬性(想一想 MathScore.__init__裏面那噁心的判斷語句),還有最重要的是:代碼重用!!!

簡而言之:經過單個 descriptor 對象,能夠更加逼真的模擬實例屬性,而且能夠實現對宿主類實例的多個實例屬性進行操做。

O.O! 若是你又秒懂了,那麼你能夠直接跳到下面寫評論了...

看個栗子:假如不只要判斷學生的分數是否爲負數,並且還要判學生的學號是否爲負值,使用 property 的實現方式是這樣子的:

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 實例最大的問題是:

  1. 沒法影響宿主類實例的初始化,因此咱必須在__init__ 加上那醜惡的 if ...

  2. 單個 Property 實例僅能針對宿主類實例的單個屬性,若是須要對多個屬性進行控制,則必須定義多個 Property 實例, 這真是太蛋疼了!

可是自定義 descriptor 能夠很好的解決這個問題,看下實現:

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):
        #這裏並未建立實例屬性 std_id 和 score, 而是調用 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__ 內終於沒了 if ,代碼也比上面的簡潔很多,可是功能一個很多,且實例之間不會相互影響:

zhangsan = MathScore(12, 50)
Description set <__main__.MathScore object at 0x0000022E47C46D68> 12
Description set <__main__.MathScore object at 0x0000022E47C46D68> 50

lisi = MathScore(13, 90)
Description set <__main__.MathScore object at 0x0000022E47C46160> 13
Description set <__main__.MathScore object at 0x0000022E47C46160> 90

wangerma = MathScore(-13, 70)
Description set <__main__.MathScore object at 0x0000022E47C46DD8> -13


ValueError: Can't be negative number!


zhangsan.score
Description get <__main__.MathScore object at 0x0000022E47C46D68>
Out[116]: 50

zhangsan.score = 80
Description set <__main__.MathScore object at 0x0000022E47C46D68> 80

lisi.score
Description get <__main__.MathScore object at 0x0000022E47C46160>
Out[118]: 90

事實上,MathScore 多個實例的同一個屬性,都是經過單個 MathScore 類的相應類屬性(也即 NonNegative 實例)操做的,這同 property 一致,但它又是怎麼克服 Property 的兩個不足的呢?祕訣有三個:

  1. Property 實例本質上是藉助類屬性,變向對實例屬性進行操做,而 NonNegative 實例則是徹底經過類屬性模擬實例屬性,所以實例屬性其實根本不存在;

  2. NonNegative 實例使用字典記錄每一個 MathScore 實例及其對應的屬性值,其中 key 爲 MathScore 實例名:好比 score 實例就是使用 dic = {‘Zhangsan’:50, ‘Lisi’:90} 記錄每一個實例對應的 score 值,從而確保能夠實現對 MathScore 實例屬性的模擬;

  3. MathScore 經過在__init__內直接調用類屬性,從而實現對實例屬性初始化賦值的模擬,而 Property 則不可能,由於 Property 實例(也即MathScore的類屬性)是真實的操做 MathScore 實例傳入的實例屬性以達到目的,但若是在初始化程序中傳入的不是實例屬性,而是類屬性(也即 Property 實例自己),則會陷入無限遞歸(PS:想一下若是將前一個property 實例實現中的self.__score 改爲這裏的 self.score 會發生什麼)。

這三點看的似懂非懂,不要緊,來個比喻:

每一個 descriptor 實例(MathScore.score 和 MathScore.std_id)都是類做用域裏的一個籃子,籃子裏放着寫着每一個 MathScore 實例名字的盒子(‘zhangsan’,’lisi‘),同一個籃子裏的盒子只記錄一樣屬性的值(好比score籃子裏的盒子只記錄分數值),當 MathScore 的實例對相應屬性進行操做時,則找到對應的籃子,取出標有該實例名字的盒子,並對其進行操做。

所以,實例對應的屬性,壓根不在實例本身的做用域內,而是在類做用域的籃子裏,只不過咱們能夠經過 xiaoming.score 這樣的方式進行操做而已,因此其實際的調用的邏輯是這樣的:下圖右側的實例分別經過紅線和黑線對score和std_id 進行操做,他們首先經過類調用相應的類屬性,而後類屬性經過對應的 descriptor 實例做用域對操做進行處理,並返回給類屬性相應結果,最後讓實例感知到。

圖片描述

看到這裏,不少童鞋可能不淡定了,由於你們都知道在 Python 中採起 xiaoming.score = 10 這樣的賦值方式,若是 xiaoming 沒有 score 這樣的實例屬性,一定會自動建立該實例屬性,怎麼會去調用 MathScore 的 score 呢?

首先,要鼓掌!!! 給想到這點的童鞋點贊!!!其實上面在說 Property 的時候這個問題就產生了。

其次,Python 爲了實現 discriptor 確實對屬性的調用順序作出了相應的調整,這些將會「Python 的 descriptor(下)」中介紹。

參考資料

一、如何理解 Python 的 Descriptor?
二、python中基於descriptor的一些概念(上)
三、python中基於descriptor的一些概念(下)
四、descriptor 的官方文檔
五、Python描述符(descriptor)解密
六、class property 的官方文檔

相關文章
相關標籤/搜索