在 Python 面向?qū)ο缶幊讨校悓傩缘脑L問和控制非常靈活。為了支持屬性的動態(tài)訪問、驗證、緩存和代理行為,Python 提供了描述符協(xié)議(Descriptor Protocol),是 Python 中實現(xiàn)訪問控制、延遲加載、ORM 字段、property 裝飾器等機制的核心基礎(chǔ)。
描述符提供了比傳統(tǒng) getter/setter 更統(tǒng)一、更強大的能力,是 Python 對象模型中最重要的機制之一。理解描述符,能夠讓你真正掌握 Python 的屬性訪問機制。
一、傳統(tǒng)的 Setter/Getter/Deleter
假設(shè)我們有一個類 BankAccount,希望控制銀行賬戶余額 _balance 的訪問方式:
del self._balance使用方式:
# account.set_balance(-50) # 會拋出 ValueError為什么說這是“傳統(tǒng)的”?
? 必須顯式調(diào)用 get_x / set_x,不夠 Pythonic。
? 無法用自然的屬性語法(obj.attr)。
? 冗長、繁瑣,不符合“簡單即美”的 Python 哲學(xué)。
因此,Python 提供了更高級的方案 —— @property。
二、Python 的進化:@property 裝飾器
@property 允許你用屬性語法訪問方法:
del self._balance使用方式:
del account.balance # 輸出:Deleting balance!我們也可以不使用裝飾器語法,而是顯式創(chuàng)建 property:
balance = property(get_balance, set_balance, del_balance)裝飾器是語法糖,本質(zhì)和顯式寫法等價!
三、描述符與描述符協(xié)議
property 不是魔法,它基于描述符協(xié)議(Descriptor Protocol)。
(1)什么是描述符
只要一個對象實現(xiàn)了以下任意方法,它就是描述符:
? __get__(self, instance, owner)
? __set__(self, instance, value)
? __delete__(self, instance)
并且當(dāng)此對象作為類屬性存在時,通過 obj.attr 訪問會自動調(diào)用這些方法。描述符允許我們“鉤住”屬性訪問過程,自定義其行為。
(2)描述符協(xié)議三方法
__get__(self, instance, owner)
讀取屬性時觸發(fā)。
參數(shù):
self:描述符實例本身。
instance:通過實例訪問時為實例,通過類訪問時為 None。
owner:擁有描述符的類(通過類訪問時)。
__set__(self, instance, value)
設(shè)置屬性時觸發(fā)。
__delete__(self, instance)
刪除屬性時觸發(fā)。
所有 property、方法綁定、ORM 字段、cached_property 都基于描述符協(xié)議。
(3)property 與描述符的關(guān)系
當(dāng)我們使用 @property 時,本質(zhì)上是在創(chuàng)建一個 property 描述符實例。
以下是 property 的簡化原理:
self.fdel(instance)property 就是一個實現(xiàn)了完整描述符協(xié)議的類。
property 默認是“非數(shù)據(jù)描述符”;只有在定義了 fset 或 fdel 時才成為“數(shù)據(jù)描述符”。
四、數(shù)據(jù)描述符與非數(shù)據(jù)描述符
Python 將描述符分為兩類,它們的優(yōu)先級不同。
(1)數(shù)據(jù)描述符(Data Descriptor)
指的是定義了 __set__ 或 __delete__ 的描述符。
如:
? property(具有 setter 或 deleter)
? 自定義描述符實現(xiàn)了 __set__
? ORM 字段、typed 屬性驗證描述符
優(yōu)先級:數(shù)據(jù)描述符優(yōu)先于實例屬性。
示例:
print(a.x) # get —— 實例屬性不會覆蓋數(shù)據(jù)描述符(2)非數(shù)據(jù)描述符(Non-Data Descriptor)
指的是只實現(xiàn)了 __get__ 的描述符。
如:
? 普通方法(function)
? property(無 setter / deleter)
優(yōu)先級:實例屬性優(yōu)先于非數(shù)據(jù)描述符。
示例:
print(a.x) # 100 —— 實例字典覆蓋了非數(shù)據(jù)描述符優(yōu)先級總結(jié):
數(shù)據(jù)描述符 > 實例屬性 > 非數(shù)據(jù)描述符 > 普通類屬性示例:
print(obj.non_data_desc) # 輸出: "實例屬性"(實例屬性優(yōu)先)五、屬性查找順序
當(dāng)執(zhí)行 obj.attr 時,Python 實際執(zhí)行(簡化邏輯):
1、在類(type(obj)) 中查找 attr;如果找到且是數(shù)據(jù)描述符 → 調(diào)用其 __get__ 并返回結(jié)果。
2、否則查找實例字典 obj.__dict__;如果存在 → 返回該值。
3、若類屬性是非數(shù)據(jù)描述符 → 調(diào)用其 __get__ 并返回結(jié)果。
4、否則返回類屬性本身。
5、若都找不到 → 如果對象實現(xiàn)了 __getattr__ 則調(diào)用它。
設(shè)置屬性時:
? 若存在數(shù)據(jù)描述符 → 調(diào)用其 __set__
? 否則寫入實例字典
刪除屬性時:
? 若存在數(shù)據(jù)描述符 → 調(diào)用其 __delete__
? 否則從實例字典刪除
示例:完整的查找規(guī)則演示
print(demo.class_attr) # 類屬性這種精細的屬性查找順序與描述符優(yōu)先級機制,使得 Python 在實現(xiàn)面向?qū)ο筇匦詴r既靈活又高效。
六、自定義描述符
下面構(gòu)造一個完整的年齡驗證描述符,改進 __get__ 的容錯處理,同時演示推薦的實例數(shù)據(jù)存儲方式(使用實例字典并帶上唯一鍵)并加入 __set_name__ 支持以獲取屬性名:
將其綁定到類屬性:
使用示例:
(1)描述符實例是類屬性,不是實例屬性
描述符對象只創(chuàng)建一次(在類定義時),不應(yīng)該在描述符內(nèi)部用 self.xxx 來存儲每個實例的數(shù)據(jù),否則所有實例會共享同一份數(shù)據(jù)(通常這是錯誤的)。正確做法是將實例數(shù)據(jù)存儲在 instance.__dict__ 或使用 instance 上的獨立鍵(例如 _{name})。
(2)set_name(推薦)
自 Python 3.6 起,描述符可以實現(xiàn) __set_name__(self, owner, name),類創(chuàng)建時會被調(diào)用一次,這可以幫助描述符自動記錄它在類中對應(yīng)的屬性名,從而簡化向 instance.__dict__ 中存儲值的實現(xiàn)(見上面示例)。這是實現(xiàn)“按屬性名存儲”“不會沖突”的推薦方式。
(3)刪除屬性后再訪問
del p.age 會調(diào)用 __delete__,如果我們在 __get__ 中直接訪問 instance.__dict__ 中的鍵而該鍵不存在,會拋出 AttributeError。在實際實現(xiàn)中可以選擇更友好的行為(例如返回 None、拋出帶信息的異常或觸發(fā)延遲加載)。
七、描述符的典型應(yīng)用場景
描述符在 Python 中有廣泛的應(yīng)用。
(1)數(shù)據(jù)驗證與類型檢查
(2)延遲加載與緩存機制(如 cached_property)
(3)觀察者模式與屬性監(jiān)聽
(4)權(quán)限控制與訪問審計
(5)ORM 與數(shù)據(jù)映射(字段描述符)
(6)配置管理與依賴注入
補充說明:方法如何綁定為 bound method
Python 中的函數(shù)對象(定義在類體中的函數(shù))實現(xiàn)了 __get__(即函數(shù)對象是非數(shù)據(jù)描述符),當(dāng)通過實例訪問時,function.__get__(instance, owner) 會返回一個“綁定方法”(bound method),它把該實例作為第一個參數(shù)(self)封裝到函數(shù)上。理解這一點有助于把“方法也是描述符”與前面描述符優(yōu)先級的討論連起來。
小結(jié)
描述符協(xié)議是 Python 中控制屬性訪問的底層機制。只要一個類實現(xiàn)了 __get__、__set__ 或 __delete__,它就能作為描述符攔截屬性的讀取、寫入和刪除操作。property、方法綁定、ORM 字段以及許多高級特性都依賴描述符協(xié)議。
理解描述符的關(guān)鍵在于區(qū)分數(shù)據(jù)描述符和非數(shù)據(jù)描述符,并掌握它們在屬性查找順序中的不同優(yōu)先級。__set_name__ 是現(xiàn)代描述符實現(xiàn)中非常有用的鉤子(Python 3.6+),推薦在自定義描述符中使用它來管理實例字典的鍵。
通過自定義描述符,我們可以構(gòu)建高度可復(fù)用、可擴展的屬性管理邏輯,使得類的行為更加靈活與優(yōu)雅;描述符不僅是 Python 的底層機制,更是構(gòu)建大型系統(tǒng)與框架的基礎(chǔ)組件。
![]()
“點贊有美意,贊賞是鼓勵”
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
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.