在 Python 的對象模型中,描述符對象(Descriptor Objects)是支撐語言動態特性的核心機制之一。從最基礎的屬性訪問,到復雜的元編程框架(如 Django ORM、SQLAlchemy、Pydantic 的字段系統),描述符始終處于幕后,卻決定著屬性系統的最終行為。
如果說 __dict__ 體系提供了屬性數據的靜態存儲結構,那么描述符對象就是介入這一結構之上的動態訪問控制層。
需要強調的是,描述符不是特殊語法或內建魔法,而是完全遵循 Python 對象模型的普通對象。
一、描述符對象的概念
(1)描述符是對象
在 Python 中,一切皆對象。描述符也不例外:
? 它是某個類的實例
? 擁有自身的類型、屬性與方法
? 可以被賦值、傳遞并存儲于 __dict__ 中
d = Descriptor()在這一層面上,d 與任何普通對象并無區別。
(2)描述符語義的由來
描述符之所以獲得特殊語義,并非源于其“身份”,而在于其實現了特定的協議方法,并且位于類屬性位置。
當一個對象同時滿足以下條件時,在屬性訪問過程中,就會被解釋器識別為描述符對象:
? 實現 __get__()、__set__()、__delete__() 中至少一個
? 作為類屬性存在于另一個類的 __dict__ 中
二、描述符的存儲位置與作用范圍
(1)描述符的存儲位置
描述符對象在參與屬性訪問控制時,必須作為類屬性存在于另一個類對象的 __dict__ 中。
x = D() # 描述符對象存放在 A.__dict__ 中此處的 D() 是一個普通對象,但由于它位于 A.__dict__ 中,因此進入屬性查找鏈。
(2)描述符的作用對象
盡管描述符存在于類級別,但其控制的卻是:
? 實例屬性的訪問
? 類屬性的訪問行為(當 instance is None)
比如:
print(a.x) # 輸出:descriptor因為該訪問會被解釋為:
A.__dict__['x'].__get__(a, A)從語言規范角度看,描述符對象本質上是對的實現。這些協議方法不是“魔法”,而是 Python 在屬性查找過程中主動調用的標準接口。
三、描述符對象的分類
根據是否攔截屬性寫入或刪除操作,描述符可分為兩類:數據描述符(Data Descriptor)和非數據描述符(Non-data Descriptor)。
(1)數據描述符
定義:實現了 __set__() 和 / 或 __delete__(),通常同時實現了 __get__() 方法。
行為特征:在屬性查找順序中優先級高于實例 __dict__,因此實例無法通過同名屬性繞過其控制。
示例:
obj.__dict__[self.storage_name] = value作為類屬性使用:
balance = Positive()訪問行為驗證:
print(a.__dict__) # 輸出 {'_balance': 100, 'balance': -999}在 Python 的世界里,沒有什么能完全阻止一個想要直接操作 __dict__ 的開發者,但描述符能確保通過“正規途徑”(即 a.balance = val)進入的數據一定是合法的。真正的保護應將存儲名(如 _balance)與屬性名(balance)分離。
(2)非數據描述符
定義:僅實現 __get__() 方法。
行為特征:優先級低于實例 __dict__,因此可被實例屬性遮蔽。
示例:
return value作為類屬性使用:
return 42訪問行為驗證:
print(d.value) # 第二次:直接從 d.__dict__ 取值,42,不再觸發描述符以上示例利用非數據描述符優先級低于實例 __dict__ 的特性實現“惰性求值”:首次訪問時觸發計算并將結果緩存至實例 __dict__ ;后續訪問則因實例屬性“遮蔽”了描述符而直接讀取緩存,從而有效避免重復計算,優化運行性能。
四、Python 內置的描述符對象
Python 中的大量核心對象,本身就是描述符對象。
(1)函數對象:非數據描述符
類中定義的函數對象本身是非數據描述符。通過其 __get__() 方法,Python 實現了實例方法的自動綁定。
a = A()當訪問方法 foo:
a.foo本質是:
A.__dict__['foo'].__get__(a, A)從而生成綁定方法(Bound Method)。
(2)@property:標準數據描述符
@property 返回的是標準的數據描述符對象(實現了 __get__()、__set__() 和 __delete__()),用于將屬性訪問映射為函數調用。
示例:
self._age = value訪問行為:
p.age = 30 # 調用 property.__set__可以這樣說,@property 是描述符機制的官方封裝版本。
(3)@classmethod 與 @staticmethod
這兩個裝飾器均返回描述符對象,分別實現對類對象或函數本身的不同綁定策略。
示例:
return "no binding"訪問驗證:
print(Demo().static_method()) # 仍不綁定classmethod 的描述符在 __get__() 中綁定 owner。staticmethod 的描述符在 __get__() 中直接返回函數。二者都是描述符對象,只是綁定策略不同。
五、描述符對象在屬性查找鏈中的位置
當執行 obj.attr 時,Python 的查找順序為:
1、類 __dict__ 中的數據描述符
2、實例 obj.__dict__
3、類 __dict__ 中的非數據描述符
4、類 __dict__ 中的普通屬性
5、__getattr__() 方法
描述符的“權力”并非絕對,而是由協議與順序共同決定的。
六、描述符的現代最佳實踐:__set_name__
Python 3.6 之后,引入了:
__set_name__(self, owner, name)__set_name__() 方法在類創建階段被自動調用,使描述符對象能夠獲知自身的屬性名與所屬類。這是當前描述符實現的標準范式。
示例:
setattr(obj, self.storage_name, value)描述符作為類屬性使用:
salary = Typed()此時在類創建過程中,解釋器會隱式執行:
Typed.__set_name__(Employee, "salary")實際訪問行為如下:
print(e.salary) # 輸出:8000底層狀態:
e.__dict__ == {"_age": 30, "_salary": 8000}實際數據存儲在實例的 __dict__ 中,而訪問路徑始終經過類 __dict__ 中的描述符對象。
上例說明:
? Typed() 本身是一個普通對象。
? 它存在于 Employee.__dict__。
? 通過 __set_name__ 獲得屬性名。
? 通過 __get__ / __set__ 管理實例數據。
? 實例并不直接暴露真實存儲字段。
這一結構正是現代描述符實現的標準范式,也是 ORM、字段系統、類型系統中最常見的設計基礎。
小結
描述符對象是 Python 屬性系統中的關鍵組成部分。它們以普通對象的形式存在于類 __dict__ 中,通過實現特定協議方法參與屬性查找過程,從而實現對屬性訪問行為的精細控制。理解描述符,有助于全面把握 Python 對象模型與屬性機制的設計思想。
![]()
“點贊有美意,贊賞是鼓勵”
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.