標籤:python 自動 ali identity 異常 turn color rop 擴充
描述符是對多個屬性運用相同存取邏輯的一種方式,,是實現了特性協議的類,這個協議包括了__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類的執行個體即為託管類執行個體
- 儲存屬性:受管理的執行個體中儲存自身Managed 屬性的屬性。在圖1-1中,LineItem執行個體的weight和price屬性是儲存屬性。這種屬性與描述符屬性不同,描述符屬性都是類屬性
- Managed 屬性:託管類中由描述符執行個體處理的公開屬性,值儲存在儲存屬性中。也就是說,描述符執行個體和儲存屬性為Managed 屬性建立了基礎
下面,讓我們來看一個例子
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 uuidclass 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 uuidclass 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()特性Factory 方法,使之不需要傳入storage_name
import uuiddef 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.priceraisins = 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 abcimport uuidclass 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 valueclass 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屬性描述符(一)