1、屬性描述符的基礎介紹
1.1 何為屬性描述符?
屬性描述符是一種Python語言中的特殊物件,用于定義和控制類屬性的行為,屬性描述符可以通過定義__get__、__set__、__delete__方法來控制屬性的讀取、賦值和洗掉操作,
通過使用屬性描述符,可以實作對屬性的訪問控制、型別檢查、計算屬性等高級功能,
如果一個物件定義了這些方法中的任何一個,它就是一個描述符,
看完上面的文字描述,是不是感覺一頭霧水,沒關系,接下來通過一個簡單的案例來講解屬性描述符的作用,
1.2 為什么需要屬性描述符?
假設我們現在要做一個成績管理系統,在定義學生類時,我們可能這樣寫:
class Student(object):
def __init__(self, name, age, cn_score, en_score):
self.name = name
self.age = age
self.cn_score = cn_score
self.en_score = en_score
def __str__(self):
return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
xiaoming = Student("xiaoming", 18, 70, 55)
print(xiaoming)
1.2.1 init函式中做引數校驗
因為python是動態語言型別,不像靜態語言那樣,可以給引數指定型別,所以在傳參時,無法得知引數是否正確,比如,當cn_score傳入的值為字串時,程式并不會報錯,這個時候,一般就會想到對傳入的引數做校驗,當傳入的引數不符合要求時,拋錯,
class Student(object):
def __init__(self, name, age, cn_score, en_score):
self.name = name
if not isinstance(age, int):
raise TypeError("age must be int")
if age <= 0:
raise ValueError("age must be greater than 0")
self.age = age
if not isinstance(cn_score, int):
raise TypeError("cn_score must be int")
if 0 <= cn_score <= 100:
raise ValueError("cn_score must be between 0 and 100")
self.cn_score = cn_score
if not isinstance(en_score, int):
raise TypeError("en_score must be int")
if 0 <= en_score <= 100:
raise ValueError("en_score must be between 0 and 100")
self.en_score = en_score
def __str__(self):
return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
xiaoming = Student("xiaoming", -1, 70, 55)
print(xiaoming)
雖然上面的代碼可以實作引數校驗,但是過多的邏輯判斷在初始化函式里面,會導致函式特別臃腫,當增加新的引數時,需要增加邏輯判斷,一方面重復代碼增加,另外也不符合開閉原則,
1.2.2 使用property做引數校驗
這個時候該怎么處理呢,我們知道python的內置函式 property可用于裝飾方法,使方法之看起來像屬性一樣,我們可以借助此函式來優化代碼,優化后如下:
class Student(object):
def __init__(self, name, age, cn_score, en_score):
self.name = name
self.age = age
self.cn_score = cn_score
self.en_score = en_score
@property
def age(self):
return self.age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("age must be int")
if value <= 0:
raise ValueError("age must be greater than 0")
self.age = value
@property
def cn_score(self):
return self.cn_score
@cn_score.setter
def cn_score(self, value):
if not isinstance(value, int):
raise TypeError("cn_score must be int")
if 0 <= value <= 100:
raise ValueError("cn_score must be between 0 and 100")
self.cn_score = value
@property
def en_score(self):
return self.en_score
@en_score.setter
def en_score(self, value):
if not isinstance(value, int):
raise TypeError("en_score must be int")
if 0 <= value <= 100:
raise ValueError("en_score must be between 0 and 100")
self.en_score = value
def __str__(self):
return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
xiaoming = Student("xiaoming", -1, 70, 55)
print(xiaoming)
現在代碼看起來已經挺不錯的了,確實,但是想想平常開發中,我們使用Diango 的 ORM 時,定義model時,只需要定義 modle 的屬性,就可以使其完成引數的校驗,比如ip = models.CharField(max_length=20, db_index=True, verbose_name='IP'),這是怎么做到的呢?
1.2.3 使用屬性描述符做引數校驗
其實,Django 是使用到了Python的屬性描述符 __get__、__set__,接下來,我們使用上面的兩個方法,來進行改造,代碼如下:
class Score:
def __init__(self, score):
self.score = score
def __get__(self, instance, owner):
return self.score
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError("value must be int")
if 0 <= value <= 100:
self.score = value
else:
raise ValueError("value must be between 0 and 100")
class Age:
def __init__(self, age):
self.age = age
def __get__(self, instance, owner):
return self.age
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError("age must be int")
if value <= 0:
raise ValueError("age must be greater than 0")
self.age = value
class Student(object):
age = Age(0)
cn_score = Score(0)
en_score = Score(0)
def __init__(self, name, _age, _cn_score, _en_score):
self.name = name
# 通過這里引數名稱的區別,我們可以更加明確的知道,是呼叫
self.age = _age
self.cn_score = _cn_score
self.en_score = _en_score
def __str__(self):
return "Student: {},age:{},cn_score:{},en_score:{}".format(self.name, self.age, self.cn_score, self.en_score)
xiaoming = Student("xiaoming", -1, 70, 55)
print(xiaoming)
通過上面的定義,也能夠實作之前的功能,而且代碼重用度更高,看起來也更加簡潔,
1.3 屬性描述符分類
常見的屬性描述符包括資料描述符和非資料描述符,
- 資料描述符
是指同時定義了
__get__、__set__方法的屬性描述符,它可以完全控制屬性的讀寫操作,
- 非資料描述符
是指只定義了
__get__方法的屬性描述符,它只能控制屬性的讀取操作,而不能控制屬性的賦值和洗掉操作,
2、屬性描述符的詳細介紹
2.1 屬性描述符的呼叫時機
描述符本質就是一個新式類,在這個新式類中,至少實作了__get__、__set__、__delete__中的一個,這也被稱為描述符協議,
-
__get__():呼叫一個屬性時,觸發 -
__set__():為一個屬性賦值時,觸發 -
__delete__():采用del洗掉屬性時,觸發
通過下面的例子將更加清晰的知道 屬性描述符的呼叫時機,
class Age:
def __init__(self, age):
self.age = age
def __get__(self, instance, owner):
print("coming __get__")
return self.age
def __set__(self, instance, value):
print("coming __set__")
if not isinstance(value, int):
raise TypeError("age must be int")
if value <= 0:
raise ValueError("age must be greater than 0")
self.age = value
def __delete__(self, instance):
print("coming __del__")
del self.age
class Student(object):
age = Age(0)
def __init__(self, name):
self.name = name
xiaoming = Student("xiaoming")
xiaoming.age = 9
print(xiaoming.age)
del xiaoming.age
#################
結果:
coming __set__
coming __get__
coming __del__
2.2 屬性的搜索順序
這里跟屬性描述符關系不是特別大,主要是看看屬性的搜索順序,
默認的屬性訪問是從物件的字典中 get, set, 或者 delete 屬性,例如a.x的查找順序是:
a.__getattribute__() -> a.__dict__['age'] -> type(a).__dict__['age'] -> type(a)的基類(不包括元類)-> a.__getattr__ -> 拋錯

如果查找的值是物件定義的描述方法之一,python可能會呼叫描述符方法來多載默認行為,發生在這個查找環節的哪里取決于定義了哪些描述符方法,
1、非資料描述器,實體的屬性搜索順序如下:
a.__getattribute__() -> a.__dict__['age'] -> a.__get__() -> type(a).__dict__['age'] -> type(a)的基類(不包括元類)-> a.__getattr__ -> 拋錯,

class Age(object):
def __get__(self, instance, owner):
print("coming __get__")
return "__get__"
# def __set__(self, instance, value):
# print("coming __set__")
# self.age = value
class A2(object):
age = 10
def __init__(self):
self.age = 1000
class A(object):
age = Age()
def __init__(self):
super().__init__()
# def __getattribute__(self, item):
# print("coming __getattribute__")
# return "xxx"
#
def __getattr__(self, item):
print("coming __getattr__")
return "__getattr__"
a = A()
print(a.age)
2、資料描述器,實體的屬性搜索順序如下:
a.__getattribute__() -> a.__get__() -> a.__dict__['age'] -> type(a).__dict__['age'] -> type(a)的基類(不包括元類)-> a.__getattr__ -> 拋錯,

class Age(object):
def __get__(self, instance, owner):
print("coming __get__")
return "__get__"
def __set__(self, instance, value):
print("coming __set__")
self.age = value
class A2(object):
def __init__(self):
self.age = 1000
class A(object):
age = Age()
def __init__(self):
self.age = 100
super().__init__()
# def __getattribute__(self, item):
# print("coming __getattribute__")
# return "xxx"
#
def __getattr__(self, item):
print("coming __getattr__")
return "__getattr__"
a = A()
print(a.age)
參考鏈接:
【案例講解】Python為什么要使用描述符?
[屬性描述符:__get__函式、__set__函式和__delete_函式](
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549839.html
標籤:Python
