属性python_Python属性描述符(一)
描述符是對多個屬性運用相同存取邏輯的一種方式,,是實現了特性協議的類,這個協議包括了__get__、__set__和__delete__方法。property類實現了完整的描述符協議。通常,可以只實現部分協議,如只實現了__get__或__set__,而不必把__get__、__set__和__delete__全部實現
現在,讓我們用描述符協議升級上一個章節Python動態屬性和特性(二)的LineItem類
圖1-1
我們將定義一個Quantity類,LineItem類會用到兩個Quantity實例:一個用于管理 weight屬性,另一個用于管理 price屬性。weight這個屬性出現了兩次,但兩次都有不同,一個是LineItem的類屬性,另一個是各個LineItem 對象的實例屬性,同理price
現在,讓我們看一些定義:
描述符類:實現描述符協議的類,比如__set__、__get__或__delete__方法,如圖1-1的Quantity類
托管類:把描述符實例聲明為類屬性的類,如圖1-1中的LineItem類中的weight和price都為類屬性,都為Quantity描述符類的實例
描述符實例:描述符類的各個實例, 聲明為托管類的類屬性,如LineItem類中的weight和price屬性
托管實例:托管類的實例,在圖1-1中,LineItem類的實例即為托管類實例
儲存屬性:托管實例中存儲自身托管屬性的屬性。在圖1-1中,LineItem實例的weight和price屬性是儲存屬性。這種屬性與描述符屬性不同,描述符屬性都是類屬性
托管屬性:托管類中由描述符實例處理的公開屬性,值存儲在儲存屬性中。也就是說,描述符實例和儲存屬性為托管屬性建立了基礎
下面,讓我們來看一個例子
class Quantity: # <3>
def __init__(self, storage_name): # <4>
self.storage_name = storage_name
def __set__(self, instance, value): # <5>
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity('weight') # <1>
price = Quantity('price')
def __init__(self, description, weight, price): # <2>
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
我們將上面的代碼與之前的定義對應起來,首先是Quantity類,我們之前說過,只要實現了__set__、__get__或__delete__方法的類,就是描述符類,所以Quantity毫無疑問的是描述符類,再來是LineItem,根據之前的定義,托管類中的類屬性,是描述符類的實例,LineItem類的weight和price兩個類屬性都是Quantity描述符類的實例,所以LineItem類即為托管類,再來,我們根據代碼中的標號分析一下代碼:
LineItem中兩個屬性weight和price為描述符實例
當實例化一個LineItem對象時,需傳入weight和price參數,由于這兩個屬性實現了描述符協議,所以關于weight和price的讀值、取值或者刪除值都可能關聯到對應同名類屬性Quantity實例中方法,由于Quantity類中只實現了__set__方法,所以這里讀值和刪除值不會觸發Quantity實例中的方法
Quantity為描述符類
Quantity實例有個storage_name屬性,這是托管實例中存儲值的屬性的名稱
當我們要設置LineItem實例中的weight或者price屬性,則會觸發__set__方法,這個方法中self為描述符實例,即為LineItem類中的weight或price的Quantity實例,instance為托管類實例,即為LineItem實例,value是我們要設置的值,如果判斷value大于0,則將其屬性名和屬性值設置到instance.__dict__字典里
現在讓我們來測試這個類,我們故意將傳入的price設為0:
truffle = LineItem('White truffle', 100, 0)
運行結果:
Traceback (most recent call last):
……
ValueError: value must be > 0
可以看到,在設置值得時候確實觸發了__set__方法
另外還要重復聲明一點:__set__方法中的參數,self和instance分別為描述符實例和托管類實例,instance代表要設置屬性的那個對象,而self(描述符實例)則保存了要設置屬性的屬性名,在上個例子中,如果我們在__set__方法要設置LineItem實例只能用這樣的方式:
instance.__dict__[self.storage_name] = value
如果嘗試用setattr()方法來賦值
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
測試:
truffle = LineItem('White truffle', 100, 10)
運行結果:
Traceback (most recent call last):
……
RecursionError: maximum recursion depth exceeded
我們會發現,如果用setattr()方法來賦值,會產生堆棧異常,為什么會這樣呢?假設obj是LineItem實例,obj.price = 10和setattr(obj, "price", 10)一樣,都會調用__set__方法,如果用setattr()方法來設置值,會不斷調用__set__方法,最終產生堆棧異常
上面的例子,LineItem有個缺點,在托管類中每次實例化描述符時都要重復輸入屬性名,現在,讓我們再改造一下LineItem類,使得不需要輸入屬性名。為了避免在描述符實例中重復輸入屬性名,我們將每個Quantity實例中的storage_name屬性生成一個獨一無二的字符串,同時為描述符類加上__get__方法
import uuid
class Quantity:
def __init__(self): # <1>
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity)
def __get__(self, instance, owner): # <2>
return getattr(instance, self.storage_name)
def __set__(self, instance, value): # <3>
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
測試:
raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.weight, raisins.description, raisins.price)
運行結果:
10 Golden raisins 6.95
這里的Quantity描述符類在實例化時,我們不再要求需要傳入一個storage_name了,而是在初始化方法中生成一個storage_name,這個storage_name由類名和uuid生成的隨機字符串組成
我們知道,如果我們對一個實例中的屬性賦值,如果這個屬性名在類中定義為描述符實例,在賦值時會自動觸發__set__方法,而__get__方法則是在我們讀值的時候自動觸發,__get__方法除了self(描述符實例)還會傳入兩個參數,instance和owner,instance是托管類實例,owner是托管類,在我們上面的例子instance即為LineItem的實例,owner即LineItem類,當讀取實例中的一個屬性,如果這個屬性在類中定義為描述符實例,則會觸發__get__方法
在__set__方法中,我們不再調用instance.__dict__[self.storage_name] = value的方式來賦值,而是直接使用setattr()方法來賦值。上一個例子中,我們測試了如果用setattr()方法來賦值的話會出現堆棧溢出的異常,那為什么我們這里又可以用了呢?是因為,我們真正存儲屬性值的時候,用的屬性名并不是類的描述符名,而是由Python解釋器生成一個Quantity_#_{uuid}隨機字符串,而這個隨機字符串,而這個字符串并未在類中注冊為描述符實例,所以我們調用setattr(),不會再像之前那樣產生堆棧異常
這里還有一點,當我們嘗試打印一下LineItem.weight這個描述符實例
LineItem.weight
運行結果:
Traceback (most recent call last):
……
return getattr(instance, self.storage_name)
AttributeError: 'NoneType' object has no attribute '_Quantity#f9860e73'
我們會發現,訪問LineItem.weight會拋出AttributeError異常,因為在訪問LineItem.weight屬性時,同樣會調用__get__方法,這個時候instance傳入的是一個None,為了解決這個問題,我們在__get__方法中檢測,如果傳入的instance為None,則返回當前描述符實例,如果instance不為None,則返回instance中的實例屬性
import uuid
class Quantity:
def __init__(self):
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
這里我們修改另外一個章節Python動態屬性和特性(二)中的quantity()特性工廠方法,使之不需要傳入storage_name
import uuid
def quantity():
storage_name = '_{}:{}'.format('quantity', str(uuid.uuid4())[:8])
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
class LineItem:
weight = quantity()
price = quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.weight, raisins.description, raisins.price)
運行結果:
10 Golden raisins 6.95
現在,我們對比一下描述符類和特性工廠,兩種方法都可以在對屬性設值或讀取時進行一些額外的操作,哪種更好呢?這里建議使用描述符類的方式,主要有兩個原因:
描述符類可以使用子類擴展,若想重用工廠函數中的代碼,除了復制黏貼,很難有其他的辦法
使用函數屬性和閉包保持狀態相比,在類屬性和實例屬性中保持狀態更易于理解
我們通過描述符類Quantity,在訪問和設置LineItem托管實例的weight和price時進行額外的操作,現在,讓我們更進一步,新增一個description描述符實例,對當要對LineItem實例的description屬性進行設置和訪問時,也增加一些操作。這里,我們要新增一個描述符類NotBlank,在設計NotBlank的過程中,我們發現它與Quantity描述符類很像,只是驗證邏輯不同
回想Quantity的功能,我們注意到它做了兩件不同的事,管理托管實例中的存儲屬性,以及驗證用于設置那兩個屬性的值。由此可見,我們可以通過繼承的方式,來復用描述符類,這里,我們創建兩個基類:
AutoStorage:自動管理儲存屬性的描述符類
Validated:擴展 AutoStorage 類的抽象子類,覆蓋 __set__ 方法,調用必須由子類實現的validate方法
稍后我們會重寫Quantity類,并實現NotBlank類,使它繼承Validated類,只編寫validate方法,類之間的關系如圖1-2:
圖1-2
圖1-2:幾個描述符類的層次結構。AutoStorage基類負責自動存儲屬性;Validated類做驗證,把職責委托給抽象方法validate;Quantity和NonBlank是Validated的具體子類。Validated、Quantity和NonBlank 三個類之間的關系體現了模板方法設計模式。
import abc
import uuid
class AutoStorage: # <1>
def __init__(self):
cls = self.__class__
prefix = cls.__name__
identity = str(uuid.uuid4())[:8]
self.storage_name = '_{}#{}'.format(prefix, identity)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
setattr(instance, self.storage_name, value)
class Validated(abc.ABC, AutoStorage): # <2>
def __set__(self, instance, value): # <3>
value = self.validate(instance, value)
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value): # <4>
"""return validated value or raise ValueError"""
class Quantity(Validated):
"""a number greater than zero"""
def validate(self, instance, value): # <5>
if value <= 0:
raise ValueError('value must be > 0')
return value
class NotBlank(Validated):
"""a string with at least one non-space character"""
def validate(self, instance, value): # <6>
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
AutoStorage類提供了之前Quantity描述符類的大部分功能
Validated類是抽象類,不過也同時繼承了AutoStorage類
Validated類中重寫__set__方法,先通過校驗方法,再調用父類的__set__方法來存儲值
抽象方法,具體實現由子類完成
Quantity實現了父類Validated的validate方法,校驗設置的值必須大于0
NotBlank實現了父類Validated的validate方法,校驗設置的值不能為空字符串
使用Quantity和NonBlank描述符的LineItem類
class LineItem:
description = NotBlank()
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
測試新的LineItem類
raisins = LineItem(' ', 10, 6.95)
運行結果:
Traceback (most recent call last):
……
ValueError: value cannot be empty or blank
總結
以上是生活随笔為你收集整理的属性python_Python属性描述符(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 同一plan节点的targetlist和
- 下一篇: 【中文题库】CISCO CCNP题库 6