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

