詳解Python描述符的工作原理
一、前言
其實,在開發過程中,雖然我們沒有直接使用到描述符,但是它在底層卻無時不刻地被使用到,例如以下這些:
function
、bound method
、unbound method
裝飾器property
、staticmethod
、classmethod
是不是都很熟悉?
這些都與描述符有著千絲萬縷的關系,這篇文章我們就來看一下描述符背後的工作原理。
二、什麼是描述符?
在解釋什麼是「描述符」之前,我們先來看一個簡單的例子。
這個例子非常簡單,我們在類 A
中定義瞭一個類屬性 x
,然後打印它的值。
其實,除瞭直接定類屬性之外,我們還可以這樣定義一個類屬性:
仔細看,這次類屬性 x
不再是一個具體的值,而是一個類 Ten
。Ten
中定義瞭一個 __get__
方法,返回具體的值。
在 Python 中,允許把一個類屬性,托管給一個類,這個屬性就是一個「描述符」。
換句話說,「描述符」是一個「綁定行為」的屬性。
怎麼理解這句話?
回憶一下,我們開發時,一般把「行為」叫做什麼?是的,「行為」一般指的是一個方法。
所以我們也可以把「描述符」理解為:對象的屬性不再是一個具體的值,而是交給瞭一個方法去定義。
可以想一下,如果我們用一個方法去定義一個屬性,這麼做的好處是什麼?
有瞭方法,我們就可以在方法內實現自己的邏輯,最簡單的,我們可以根據不同的條件,在方法內給屬性賦予不同的值,就像下面這樣:
三、描述符協議
瞭解瞭描述符的定義,現在我們把重點放到托管屬性的類上。
其實,一個類屬性想要托管給一個類,這個類內部實現的方法不能是隨便定義的,它必須遵守「描述符協議」,也就是要實現以下幾個方法:
__get__(self, obj, type=None) __set__(self, obj, value) __delete__(self, obj)
隻要是實現瞭以上幾個方法的其中一個,那麼這個類屬性就可以稱作描述符。
另外,描述符又可以分為「數據描述符」和「非數據描述符」:
隻定義瞭 __get___,叫做非數據描述符 除瞭定義 __get__ 之外,還定義瞭 __set__ 或 __delete__,叫做數據描述符
它們兩者有什麼區別,我會在下面詳述。
現在我們來看一個包含 __get__
和 __set__
方法的描述符例子:
在這例子中,類屬性 age
是一個描述符,它的值取決於 Age
類。
從輸出結果來看,當我們獲取或修改 age
屬性時,調用瞭 Age
的 __get__
和 __set__
方法:
- 當調用
p1.age
時,__get__
被調用,參數obj
是Person
實例,type
是type(Person)
- 當調用
Person.age
時,__get__
被調用,參數obj
是None
,type
是type(Person)
- 當調用
p1.age = 25
時,__set__
被調用,參數obj
是Person
實例,value
是25 - 當調用
p1.age = -1
時,__set__
沒有通過校驗,拋出ValueError
其中,調用 __set__
傳入的參數,我們比較容易理解,但是對於 __get__
方法,通過類或實例調用,傳入的參數是不同的,這是為什麼?
這就需要我們瞭解一下描述符的工作原理。
四、描述符的工作原理
要解釋描述符的工作原理,首先我們需要先從屬性的訪問說起。
在開發時,不知道你有沒有想過這樣一個問題:通常我們寫這樣的代碼 a.b
,其背後到底發生瞭什麼?
這裡的 a
和 b
可能存在以下情況:
1.a 可能是一個類,也可能是一個實例,我們這裡統稱為對象
2.b 可能是一個屬性,也可能是一個方法,方法其實也可以看做是類的屬性
其實,無論是以上哪種情況,在 Python 中,都有一個統一的調用邏輯:
1.先調用 __getattribute__
嘗試獲得結果
2.如果沒有結果,調用 __getattr__
用代碼表示就是下面這樣:
我們這裡需要重點關註一下 __getattribute__
,因為它是所有屬性查找的入口,它內部實現的屬性查找順序是這樣的:
1.要查找的屬性,在類中是否是一個描述符
2.如果是描述符,再檢查它是否是一個數據描述符
3.如果是數據描述符,則調用數據描述符的 __get__
4.如果不是數據描述符,則從 __dict__
中查找
5.如果 __dict__
中查找不到,再看它是否是一個非數據描述符
6.如果是非數據描述符,則調用非數據描述符的 __get__
7.如果也不是一個非數據描述符,則從類屬性中查找
8.如果類中也沒有這個屬性,拋出 AttributeError
異常
寫成代碼就是下面這樣:
如果不好理解,你最好寫一個程序測試一下,觀察各種情況下的屬性的查找順序。
到這裡我們可以看到,在一個對象中查找一個屬性,都是先從 __getattribute__
開始的。
在 __getattribute__
中,它會檢查這個類屬性是否是一個描述符,如果是一個描述符,那麼就會調用它的 __get__
方法。但具體的調用細節和傳入的參數是下面這樣的:
如果 a
是一個實例,調用細節為:
所以我們就能看到上面例子輸出的結果。
五、數據描述符和非數據描述符
瞭解瞭描述符的工作原理,我們繼續來看數據描述符和非數據描述符的區別。
從定義上來看,它們的區別是:
- 隻定義瞭
__get___
,叫做非數據描述符 - 除瞭定義
__get__
之外,還定義瞭__set__
或__delete__
,叫做數據描述符
此外,我們從上面描述符調用的順序可以看到,在對象中查找屬性時,數據描述符要優先於非數據描述符調用。
在之前的例子中,我們定義瞭 __get__
和 __set__
,所以那些類屬性都是數據描述符。
我們再來看一個非數據描述符的例子:
這段代碼,我們定義瞭一個相同名字的屬性和方法 foo
,如果現在執行 A().foo
,你覺得會輸出什麼結果?
答案是 abc
。
為什麼打印的是實例屬性 foo
的值,而不是方法 foo
呢?
這就和非數據描述符有關系瞭。
我們執行 dir(A.foo)
,觀察結果:
看到瞭嗎?A
的 foo
方法其實實現瞭 __get__
,我們在上面的分析已經得知:隻定義 __get__
方法的對象,它其實是一個非數據描述符,也就是說,我們在類中定義的方法,其實本身就是一個非數據描述符。
所以,在一個類中,如果存在相同名字的屬性和方法,按照上面所講的 __getattribute__
中查找屬性的順序,這個屬性就會優先從實例中獲取,如果實例中不存在,才會從非數據描述符中獲取,所以在這裡優先查找的是實例屬性 foo
的值。
到這裡我們可以總結一下關於描述符的相關知識點:
- 描述符必須是一個類屬性
__getattribute__
是查找一個屬性(方法)的入口__getattribute__
定義瞭一個屬性(方法)的查找順序:數據描述符、實例屬性、非數據描述符、類屬性- 如果我們重寫瞭
__getattribute__
方法,會阻止描述符的調用 - 所有方法其實都是一個非數據描述符,因為它定義瞭
__get__
六、描述符的使用場景
瞭解瞭描述符的工作原理,那描述符一般用在哪些業務場景中呢?
在這裡我用描述符實現瞭一個屬性校驗器,你可以參考這個例子,在類似的場景中去使用它。
首先我們定義一個校驗基類 Validator
,在 __set__
方法中先調用 validate
方法校驗屬性是否符合要求,然後再對屬性進行賦值。
現在,當我們對 Person
實例進行初始化時,就可以校驗這些屬性是否符合預定義的規則瞭。
七、function與method
我們再來看一下,在開發時經常看到的 function
、unbound method
、bound method
它們之間到底有什麼區別?
來看下面這段代碼:
從結果我們可以看出它們的區別:
function
準確來說就是一個函數,並且它實現瞭__get__
方法,因此每一個function
都是一個非數據描述符,而在類中會把function
放到__dict__
中存儲- 當
function
被實例調用時,它是一個bound method
- 當
function
被類調用時, 它是一個unbound method
function
是一個非數據描述符,我們之前已經講到瞭。
而 bound method
和 unbound method
的區別就在於調用方的類型是什麼,如果是一個實例,那麼這個 function
就是一個 bound method
,否則它是一個 unbound method
。
八、property/staticmethod/classmethod
我們再來看 property
、staticmethod
、classmethod
。
這些裝飾器的實現,默認是 C 來實現的。
其實,我們也可以直接利用 Python 描述符的特性來實現這些裝飾器,
property
的 Python 版實現:
除此之外,你還可以實現其他功能強大的裝飾器。
由此可見,通過描述符我們可以實現強大而靈活的屬性管理功能,對於一些要求屬性控制比較復雜的場景,我們可以選擇用描述符來實現。
到此這篇關於詳解Python描述符的工作原理的文章就介紹到這瞭,更多相關Python描述符內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!