從原理到ORM深入詳解Python屬性描述符使用方法
在Python的面向?qū)ο缶幊讨?,屬性的控制與查找是核心知識點(diǎn)之一,而屬性描述符作為實(shí)現(xiàn)屬性精細(xì)化控制的重要工具,更是ORM框架(如Django Model、SQLAlchemy)的底層實(shí)現(xiàn)基礎(chǔ)。本文將從實(shí)際開發(fā)痛點(diǎn)出發(fā),深入講解屬性描述符的定義、分類、使用方法,以及它在Python屬性查找過程中的核心作用,幫你徹底理解這一重要特性。
一、為什么需要屬性描述符?從property的局限性說起
在Python中,我們常用property裝飾器來控制屬性的獲取、設(shè)置和刪除過程,實(shí)現(xiàn)對屬性的簡單校驗(yàn)和邏輯封裝。比如我們要限制用戶類中age為整數(shù)類型、name為字符串類型,使用property可以輕松實(shí)現(xiàn):
class User:
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise ValueError("int value need")
self._age = value
但在實(shí)際開發(fā)中,尤其是在ORM場景下,一個數(shù)據(jù)模型類往往對應(yīng)數(shù)據(jù)庫的一張表,包含十幾個甚至幾十個字段,其中很多字段會有相同的類型校驗(yàn)規(guī)則(如name、email、mobile都是字符串類型,age、id都是整數(shù)類型)。
如果繼續(xù)使用property,我們需要為每個字段編寫重復(fù)的getter和setter方法,這會導(dǎo)致代碼冗余度極高,維護(hù)成本大幅增加。為了解決代碼復(fù)用的問題,Python為我們提供了更優(yōu)雅的解決方案——屬性描述符。
二、屬性描述符的定義與基礎(chǔ)使用
2.1 什么是屬性描述符?
屬性描述符的定義非常簡單:一個自定義類,只要實(shí)現(xiàn)了 __get__ 、 __set__ 、 __delete__ 三個魔法函數(shù)中的任意一個,這個類就是屬性描述符。通過這個類的實(shí)例,我們可以將屬性的控制邏輯封裝起來,實(shí)現(xiàn)多字段的邏輯復(fù)用。
2.2 基礎(chǔ)實(shí)現(xiàn):整數(shù)類型校驗(yàn)描述符
我們以實(shí)現(xiàn)整數(shù)類型校驗(yàn)為例,編寫第一個屬性描述符,解決多個整數(shù)類型字段的校驗(yàn)復(fù)用問題:
import numbers
class IntField:
# 實(shí)現(xiàn)__set__方法,完成類型校驗(yàn)
def __set__(self, instance, value):
# 校驗(yàn)是否為整數(shù)類型
if not isinstance(value, numbers.Integral):
raise ValueError("int value need")
# 校驗(yàn)是否為正數(shù)
if value < 0:
raise ValueError("positive value need")
# 將值保存到描述符自身實(shí)例,避免死循環(huán)
self.value = value
# 實(shí)現(xiàn)__get__方法,獲取屬性值
def __get__(self, instance, owner):
return self.value
2.3 在模型類中使用描述符
定義好描述符后,我們可以在模型類中直接將其作為類屬性使用,實(shí)現(xiàn)對字段的統(tǒng)一控制:
class User:
# 將IntField實(shí)例作為類屬性,實(shí)現(xiàn)age的整數(shù)校驗(yàn)
age = IntField()
# 測試正常賦值
user = User()
user.age = 30
print(user.age) # 輸出:30
# 測試賦值非整數(shù),拋出異常
user.age = "abc" # 拋出ValueError: int value need
# 測試賦值負(fù)數(shù),拋出異常
user.age = -5 # 拋出ValueError: positive value need
2.4 關(guān)鍵注意點(diǎn):避免賦值死循環(huán)
在實(shí)現(xiàn)__set__方法時,切忌將值保存到傳入的 instance (模型類實(shí)例)中,比如寫成instance.age = value。
因?yàn)楫?dāng)我們對instance.age賦值時,Python會再次調(diào)用描述符的__set__方法,從而陷入無限遞歸,最終導(dǎo)致棧溢出。正確的做法是將值保存到描述符自身的實(shí)例(self)中,在__get__方法中再從self中取出。
三、屬性描述符的分類:數(shù)據(jù)描述符與非數(shù)據(jù)描述符
根據(jù)實(shí)現(xiàn)的魔法函數(shù)不同,屬性描述符分為兩類,二者的核心區(qū)別在于在Python屬性查找過程中的優(yōu)先級不同,這也是理解屬性查找的關(guān)鍵。
3.1 數(shù)據(jù)描述符(Data Descriptor)
實(shí)現(xiàn)了 __get__ 和 __set__ 方法的描述符稱為數(shù)據(jù)描述符,是我們開發(fā)中最常用的類型,比如上文的IntField就是典型的數(shù)據(jù)描述符。
數(shù)據(jù)描述符擁有最高的屬性查找優(yōu)先級,會覆蓋實(shí)例自身的屬性值。
3.2 非數(shù)據(jù)描述符(Non-data Descriptor)
只實(shí)現(xiàn)了 __get__ 方法(未實(shí)現(xiàn)__set__)的描述符稱為非數(shù)據(jù)描述符,常見的例子是Python中的函數(shù)(函數(shù)實(shí)現(xiàn)了__get__方法,成為綁定方法)。
非數(shù)據(jù)描述符的優(yōu)先級低于實(shí)例自身的屬性值,僅在實(shí)例中未找到該屬性時才會生效。
四、Python完整的屬性查找過程:描述符的核心作用
在學(xué)習(xí)屬性描述符之前,我們對Python屬性查找的認(rèn)知通常是:先查找實(shí)例的 __dict__ ,再查找類的 __dict__ ,最后查找基類的 __dict__。
但當(dāng)引入屬性描述符后,Python的屬性查找過程會變得更加精細(xì),而這一過程也是getattr和__getattribute__兩個魔法函數(shù)的底層邏輯。當(dāng)我們使用 實(shí)例.屬性 (如 user.age )的方式訪問屬性時,等價于調(diào)用全局函數(shù) getattr(實(shí)例, 屬性名),其完整的查找順序如下:
4.1 核心查找順序
- 調(diào)用
__getattribute__:無論屬性是否存在,都會先調(diào)用類中的__getattribute__方法,這是屬性查找的入口; - 檢查數(shù)據(jù)描述符:在實(shí)例的類或基類的
__dict__中查找該屬性,若該屬性是數(shù)據(jù)描述符,則直接調(diào)用其__get__方法,返回結(jié)果; - 檢查實(shí)例自身屬性:在實(shí)例的
__dict__中查找該屬性,若找到則直接返回值; - 檢查非數(shù)據(jù)描述符/類屬性:在實(shí)例的類或基類的
__dict__中查找該屬性:- 若為非數(shù)據(jù)描述符,調(diào)用其
__get__方法返回結(jié)果; - 若不是描述符,直接返回類屬性的值;
- 若為非數(shù)據(jù)描述符,調(diào)用其
- 調(diào)用
__getattr__:若以上步驟均未找到屬性,會觸發(fā)AttributeError,此時若類中定義了__getattr__方法,會調(diào)用該方法; - 拋出異常:若未定義
__getattr__,則直接拋出AttributeError。
4.2 關(guān)鍵驗(yàn)證:數(shù)據(jù)描述符覆蓋實(shí)例屬性
數(shù)據(jù)描述符的優(yōu)先級高于實(shí)例自身屬性,即使我們手動給實(shí)例的__dict__添加該屬性,訪問時仍會優(yōu)先調(diào)用數(shù)據(jù)描述符的__get__方法:
# 延續(xù)上文的IntField和User
user = User()
user.age = 30
# 實(shí)例的__dict__為空,值保存在描述符實(shí)例中
print(user.__dict__) # 輸出:{}
# 手動給實(shí)例__dict__添加age屬性
user.__dict__['age'] = "abc"
# 訪問時仍調(diào)用數(shù)據(jù)描述符,因未給描述符賦值,會報錯
print(user.age) # 報錯:IntField has no attribute 'value'
4.3 關(guān)鍵驗(yàn)證:非數(shù)據(jù)描述符被實(shí)例屬性覆蓋
若將IntField改為非數(shù)據(jù)描述符(僅實(shí)現(xiàn)__get__),則實(shí)例自身的屬性會覆蓋描述符:
class NonDataIntField:
# 僅實(shí)現(xiàn)__get__,非數(shù)據(jù)描述符
def __get__(self, instance, owner):
return 10
class User:
age = NonDataIntField()
user = User()
# 實(shí)例賦值age,覆蓋非數(shù)據(jù)描述符
user.age = 30
print(user.age) # 輸出:30,而非描述符的10
五、屬性描述符的實(shí)際應(yīng)用:ORM框架的底層基礎(chǔ)
屬性描述符的核心價值在于屬性邏輯的封裝與復(fù)用,這也是所有Python ORM框架的底層實(shí)現(xiàn)原理。
在Django Model、SQLAlchemy等框架中,我們定義的CharField、IntegerField、EmailField等字段,本質(zhì)上都是封裝了不同校驗(yàn)規(guī)則和數(shù)據(jù)庫映射邏輯的屬性描述符:
- 字段的類型校驗(yàn)(如
CharField限制字符串)由描述符的__set__方法實(shí)現(xiàn); - 字段的數(shù)據(jù)庫字段映射(如字段長度、是否為主鍵)由描述符的初始化參數(shù)實(shí)現(xiàn);
- 字段的取值邏輯由描述符的
__get__方法實(shí)現(xiàn)。
通過屬性描述符,ORM框架將數(shù)據(jù)庫表的字段與Python類的屬性進(jìn)行了完美映射,讓我們可以用面向?qū)ο蟮姆绞讲僮鲾?shù)據(jù)庫,而無需編寫重復(fù)的校驗(yàn)和映射代碼。
六、總結(jié)
- 屬性描述符的誕生:為解決
property在多字段場景下的代碼冗余問題,實(shí)現(xiàn)屬性控制邏輯的復(fù)用; - 定義規(guī)則:實(shí)現(xiàn)
__get__、__set__、__delete__任一方法的自定義類,即為屬性描述符; - 兩大分類:實(shí)現(xiàn)
__get__+__set__的數(shù)據(jù)描述符(高優(yōu)先級)、僅實(shí)現(xiàn)__get__的非數(shù)據(jù)描述符(低優(yōu)先級); - 屬性查找核心:
實(shí)例.屬性的查找順序?yàn)椤笖?shù)據(jù)描述符 → 實(shí)例屬性 → 非數(shù)據(jù)描述符/類屬性 → getattr → 異常」; - 實(shí)際價值:Python ORM框架的底層核心,是實(shí)現(xiàn)屬性精細(xì)化控制和數(shù)據(jù)庫映射的關(guān)鍵工具。
掌握屬性描述符,不僅能讓我們寫出更優(yōu)雅、更易維護(hù)的Python代碼,更能幫助我們理解主流框架的底層實(shí)現(xiàn)邏輯,提升Python面向?qū)ο缶幊痰暮诵哪芰?。下一篇文章,我們將繼續(xù)深入Python的魔法函數(shù),講解元類編程中__new__和__init__的核心區(qū)別與使用場景。
以上就是從原理到ORM詳解Python屬性描述符使用方法的詳細(xì)內(nèi)容,更多關(guān)于Python屬性描述符的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用pandas庫讀取和寫入csv文件的實(shí)現(xiàn)示例
Python提供了多種方式來讀取和寫入CSV文件,本文主要介紹了使用pandas庫讀取和寫入csv文件的實(shí)現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2025-05-05
Python+OpenCV實(shí)現(xiàn)在圖像上繪制矩形
這篇文章主要介紹了如何利用Python和OpenCV實(shí)現(xiàn)在圖像上繪制任意大小的矩形,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2022-03-03
使用Python實(shí)現(xiàn)密碼與驗(yàn)證碼的MD5加密
在現(xiàn)代軟件開發(fā)中,數(shù)據(jù)加密是一個非常重要的環(huán)節(jié),無論是用戶密碼、驗(yàn)證碼,還是其他敏感信息,加密都是保護(hù)數(shù)據(jù)安全的關(guān)鍵手段之一,本文將通過一個具體的例子,詳細(xì)講解如何使用 Python 實(shí)現(xiàn)密碼與驗(yàn)證碼的 MD5 加密2025-02-02
Python中tkinter無法同時顯示多個image的解決方法及pack與place解析
這篇文章主要介紹了Python中tkinter無法同時顯示多個image的解決方法及pack與place解析,本文給大家分享問題原因解析及解決方案,需要的朋友可以參考下2023-02-02
python 列表推導(dǎo)和生成器表達(dá)式的使用
這篇文章主要介紹了python 列表推導(dǎo)和生成器表達(dá)式的使用方法,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2021-02-02
python讀取目錄下所有的jpg文件,并顯示第一張圖片的示例
今天小編就為大家分享一篇python讀取目錄下所有的jpg文件,并顯示第一張圖片的示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06
python3 對list中每個元素進(jìn)行處理的方法
今天小編就為大家分享一篇python3 對list中每個元素進(jìn)行處理的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-06-06
PyQt5實(shí)現(xiàn)QLineEdit正則表達(dá)式輸入驗(yàn)證器
這篇文章主要介紹了PyQt5實(shí)現(xiàn)QLineEdit正則表達(dá)式輸入驗(yàn)證器,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
Python基于pycrypto實(shí)現(xiàn)的AES加密和解密算法示例
這篇文章主要介紹了Python基于pycrypto實(shí)現(xiàn)的AES加密和解密算法,結(jié)合實(shí)例形式分析了Python使用pycrypto模塊進(jìn)行AES加密與解密操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-04-04

