目錄
- 一、前言
- 二、什么是描述符?
- 三、描述符協(xié)議
- 四、描述符的工作原理
- 五、數(shù)據(jù)描述符和非數(shù)據(jù)描述符
- 六、描述符的使用場(chǎng)景
- 七、function與method
- 八、property/staticmethod/classmethod
一、前言
其實(shí),在開發(fā)過程中,雖然我們沒有直接使用到描述符,但是它在底層卻無時(shí)不刻地被使用到,例如以下這些:
function
、bound method
、unbound method
裝飾器property
、staticmethod
、classmethod
是不是都很熟悉?
這些都與描述符有著千絲萬(wàn)縷的關(guān)系,這篇文章我們就來看一下描述符背后的工作原理。
二、什么是描述符?
在解釋什么是「描述符」之前,我們先來看一個(gè)簡(jiǎn)單的例子。
這個(gè)例子非常簡(jiǎn)單,我們?cè)陬?nbsp;A
中定義了一個(gè)類屬性 x
,然后打印它的值。
其實(shí),除了直接定類屬性之外,我們還可以這樣定義一個(gè)類屬性:
仔細(xì)看,這次類屬性 x
不再是一個(gè)具體的值,而是一個(gè)類 Ten
。Ten
中定義了一個(gè) __get__
方法,返回具體的值。
在 Python 中,允許把一個(gè)類屬性,托管給一個(gè)類,這個(gè)屬性就是一個(gè)「描述符」。
換句話說,「描述符」是一個(gè)「綁定行為」的屬性。
怎么理解這句話?
回憶一下,我們開發(fā)時(shí),一般把「行為」叫做什么?是的,「行為」一般指的是一個(gè)方法。
所以我們也可以把「描述符」理解為:對(duì)象的屬性不再是一個(gè)具體的值,而是交給了一個(gè)方法去定義。
可以想一下,如果我們用一個(gè)方法去定義一個(gè)屬性,這么做的好處是什么?
有了方法,我們就可以在方法內(nèi)實(shí)現(xiàn)自己的邏輯,最簡(jiǎn)單的,我們可以根據(jù)不同的條件,在方法內(nèi)給屬性賦予不同的值,就像下面這樣:
三、描述符協(xié)議
了解了描述符的定義,現(xiàn)在我們把重點(diǎn)放到托管屬性的類上。
其實(shí),一個(gè)類屬性想要托管給一個(gè)類,這個(gè)類內(nèi)部實(shí)現(xiàn)的方法不能是隨便定義的,它必須遵守「描述符協(xié)議」,也就是要實(shí)現(xiàn)以下幾個(gè)方法:
__get__(self, obj, type=None)
__set__(self, obj, value)
__delete__(self, obj)
只要是實(shí)現(xiàn)了以上幾個(gè)方法的其中一個(gè),那么這個(gè)類屬性就可以稱作描述符。
另外,描述符又可以分為「數(shù)據(jù)描述符」和「非數(shù)據(jù)描述符」:
只定義了 __get___,叫做非數(shù)據(jù)描述符
除了定義 __get__ 之外,還定義了 __set__ 或 __delete__,叫做數(shù)據(jù)描述符
它們兩者有什么區(qū)別,我會(huì)在下面詳述。
現(xiàn)在我們來看一個(gè)包含 __get__
和 __set__
方法的描述符例子:
在這例子中,類屬性 age
是一個(gè)描述符,它的值取決于 Age
類。
從輸出結(jié)果來看,當(dāng)我們獲取或修改 age
屬性時(shí),調(diào)用了 Age
的 __get__
和 __set__
方法:
- 當(dāng)調(diào)用
p1.age
時(shí),__get__
被調(diào)用,參數(shù) obj
是 Person
實(shí)例,type
是 type(Person)
- 當(dāng)調(diào)用
Person.age
時(shí),__get__
被調(diào)用,參數(shù) obj
是 None
,type
是 type(Person)
- 當(dāng)調(diào)用
p1.age = 25
時(shí),__set__
被調(diào)用,參數(shù) obj
是 Person
實(shí)例,value
是25
- 當(dāng)調(diào)用
p1.age = -1
時(shí),__set__
沒有通過校驗(yàn),拋出 ValueError
其中,調(diào)用 __set__
傳入的參數(shù),我們比較容易理解,但是對(duì)于 __get__
方法,通過類或?qū)嵗?/strong>調(diào)用,傳入的參數(shù)是不同的,這是為什么?
這就需要我們了解一下描述符的工作原理。
四、描述符的工作原理
要解釋描述符的工作原理,首先我們需要先從屬性的訪問說起。
在開發(fā)時(shí),不知道你有沒有想過這樣一個(gè)問題:通常我們寫這樣的代碼 a.b
,其背后到底發(fā)生了什么?
這里的 a
和 b
可能存在以下情況:
1.a 可能是一個(gè)類,也可能是一個(gè)實(shí)例,我們這里統(tǒng)稱為對(duì)象
2.b 可能是一個(gè)屬性,也可能是一個(gè)方法,方法其實(shí)也可以看做是類的屬性
其實(shí),無論是以上哪種情況,在 Python 中,都有一個(gè)統(tǒng)一的調(diào)用邏輯:
1.先調(diào)用 __getattribute__
嘗試獲得結(jié)果
2.如果沒有結(jié)果,調(diào)用 __getattr__
用代碼表示就是下面這樣:
我們這里需要重點(diǎn)關(guān)注一下 __getattribute__
,因?yàn)樗撬袑傩圆檎业娜肟?,它?nèi)部實(shí)現(xiàn)的屬性查找順序是這樣的:
1.要查找的屬性,在類中是否是一個(gè)描述符
2.如果是描述符,再檢查它是否是一個(gè)數(shù)據(jù)描述符
3.如果是數(shù)據(jù)描述符,則調(diào)用數(shù)據(jù)描述符的 __get__
4.如果不是數(shù)據(jù)描述符,則從 __dict__
中查找
5.如果 __dict__
中查找不到,再看它是否是一個(gè)非數(shù)據(jù)描述符
6.如果是非數(shù)據(jù)描述符,則調(diào)用非數(shù)據(jù)描述符的 __get__
7.如果也不是一個(gè)非數(shù)據(jù)描述符,則從類屬性中查找
8.如果類中也沒有這個(gè)屬性,拋出 AttributeError
異常
寫成代碼就是下面這樣:
如果不好理解,你最好寫一個(gè)程序測(cè)試一下,觀察各種情況下的屬性的查找順序。
到這里我們可以看到,在一個(gè)對(duì)象中查找一個(gè)屬性,都是先從 __getattribute__
開始的。
在 __getattribute__
中,它會(huì)檢查這個(gè)類屬性是否是一個(gè)描述符,如果是一個(gè)描述符,那么就會(huì)調(diào)用它的 __get__
方法。但具體的調(diào)用細(xì)節(jié)和傳入的參數(shù)是下面這樣的:
如果 a
是一個(gè)實(shí)例,調(diào)用細(xì)節(jié)為:
所以我們就能看到上面例子輸出的結(jié)果。
五、數(shù)據(jù)描述符和非數(shù)據(jù)描述符
了解了描述符的工作原理,我們繼續(xù)來看數(shù)據(jù)描述符和非數(shù)據(jù)描述符的區(qū)別。
從定義上來看,它們的區(qū)別是:
- 只定義了
__get___
,叫做非數(shù)據(jù)描述符
- 除了定義
__get__
之外,還定義了 __set__
或 __delete__
,叫做數(shù)據(jù)描述符
此外,我們從上面描述符調(diào)用的順序可以看到,在對(duì)象中查找屬性時(shí),數(shù)據(jù)描述符要優(yōu)先于非數(shù)據(jù)描述符調(diào)用。
在之前的例子中,我們定義了 __get__
和 __set__
,所以那些類屬性都是數(shù)據(jù)描述符。
我們?cè)賮砜匆粋€(gè)非數(shù)據(jù)描述符的例子:
這段代碼,我們定義了一個(gè)相同名字的屬性和方法 foo
,如果現(xiàn)在執(zhí)行 A().foo
,你覺得會(huì)輸出什么結(jié)果?
答案是 abc
。
為什么打印的是實(shí)例屬性 foo
的值,而不是方法 foo
呢?
這就和非數(shù)據(jù)描述符有關(guān)系了。
我們執(zhí)行 dir(A.foo)
,觀察結(jié)果:
看到了嗎?A
的 foo
方法其實(shí)實(shí)現(xiàn)了 __get__
,我們?cè)谏厦娴姆治鲆呀?jīng)得知:只定義 __get__
方法的對(duì)象,它其實(shí)是一個(gè)非數(shù)據(jù)描述符,也就是說,我們?cè)陬愔卸x的方法,其實(shí)本身就是一個(gè)非數(shù)據(jù)描述符。
所以,在一個(gè)類中,如果存在相同名字的屬性和方法,按照上面所講的 __getattribute__
中查找屬性的順序,這個(gè)屬性就會(huì)優(yōu)先從實(shí)例中獲取,如果實(shí)例中不存在,才會(huì)從非數(shù)據(jù)描述符中獲取,所以在這里優(yōu)先查找的是實(shí)例屬性 foo
的值。
到這里我們可以總結(jié)一下關(guān)于描述符的相關(guān)知識(shí)點(diǎn):
- 描述符必須是一個(gè)類屬性
__getattribute__
是查找一個(gè)屬性(方法)的入口
__getattribute__
定義了一個(gè)屬性(方法)的查找順序:數(shù)據(jù)描述符、實(shí)例屬性、非數(shù)據(jù)描述符、類屬性
- 如果我們重寫了
__getattribute__
方法,會(huì)阻止描述符的調(diào)用
- 所有方法其實(shí)都是一個(gè)非數(shù)據(jù)描述符,因?yàn)樗x了
__get__
六、描述符的使用場(chǎng)景
了解了描述符的工作原理,那描述符一般用在哪些業(yè)務(wù)場(chǎng)景中呢?
在這里我用描述符實(shí)現(xiàn)了一個(gè)屬性校驗(yàn)器,你可以參考這個(gè)例子,在類似的場(chǎng)景中去使用它。
首先我們定義一個(gè)校驗(yàn)基類 Validator
,在 __set__
方法中先調(diào)用 validate
方法校驗(yàn)屬性是否符合要求,然后再對(duì)屬性進(jìn)行賦值。
現(xiàn)在,當(dāng)我們對(duì) Person
實(shí)例進(jìn)行初始化時(shí),就可以校驗(yàn)這些屬性是否符合預(yù)定義的規(guī)則了。
七、function與method
我們?cè)賮砜匆幌拢陂_發(fā)時(shí)經(jīng)??吹降?nbsp;function
、unbound method
、bound method
它們之間到底有什么區(qū)別?
來看下面這段代碼:
從結(jié)果我們可以看出它們的區(qū)別:
function
準(zhǔn)確來說就是一個(gè)函數(shù),并且它實(shí)現(xiàn)了 __get__
方法,因此每一個(gè) function
都是一個(gè)非數(shù)據(jù)描述符,而在類中會(huì)把 function
放到 __dict__
中存儲(chǔ)
- 當(dāng)
function
被實(shí)例調(diào)用時(shí),它是一個(gè) bound method
- 當(dāng)
function
被類調(diào)用時(shí), 它是一個(gè) unbound method
function
是一個(gè)非數(shù)據(jù)描述符,我們之前已經(jīng)講到了。
而 bound method
和 unbound method
的區(qū)別就在于調(diào)用方的類型是什么,如果是一個(gè)實(shí)例,那么這個(gè) function
就是一個(gè) bound method
,否則它是一個(gè) unbound method
。
八、property/staticmethod/classmethod
我們?cè)賮砜?nbsp;property
、staticmethod
、classmethod
。
這些裝飾器的實(shí)現(xiàn),默認(rèn)是 C 來實(shí)現(xiàn)的。
其實(shí),我們也可以直接利用 Python 描述符的特性來實(shí)現(xiàn)這些裝飾器,
property
的 Python 版實(shí)現(xiàn):
除此之外,你還可以實(shí)現(xiàn)其他功能強(qiáng)大的裝飾器。
由此可見,通過描述符我們可以實(shí)現(xiàn)強(qiáng)大而靈活的屬性管理功能,對(duì)于一些要求屬性控制比較復(fù)雜的場(chǎng)景,我們可以選擇用描述符來實(shí)現(xiàn)。
到此這篇關(guān)于詳解Python描述符的工作原理的文章就介紹到這了,更多相關(guān)Python描述符內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- Python基礎(chǔ)詳解之描述符
- Python描述符descriptor使用原理解析
- python實(shí)現(xiàn)裝飾器、描述符
- 詳解Python中的Descriptor描述符類
- Python黑魔法Descriptor描述符的實(shí)例解析
- Python 的描述符 descriptor詳解
- 解密Python中的描述符(descriptor)