標籤:ring 不同 function one 影響 force shadow forward 其他
Python存取屬性的方式特別不對等,通過執行個體讀取屬性時,通常返回的是執行個體中定義的屬性,但如果執行個體未曾定義過該屬性,就會擷取類屬性,而為執行個體的屬性賦值時,通常會在執行個體中建立屬性,而不會影響到類本身。這種不對等的方式對描述符類也有影響。
def cls_name(obj_or_cls): # 傳入一個執行個體,返回類名 cls = type(obj_or_cls) if cls is type: cls = obj_or_cls return cls.__name__.split(‘.‘)[-1]def display(obj): cls = type(obj) if cls is type: # 如果obj是一個類,則進入該分支 return ‘<class {}>‘.format(obj.__name__) elif cls in [type(None), int]: # 如果obj是None或者數值,進入該分支 return repr(obj) else: # 如果obj是一個執行個體 return ‘<{} object>‘.format(cls_name(obj))def print_args(name, *args): pseudo_args = ‘, ‘.join(display(x) for x in args) print(‘-> {}.__{}__({})‘.format(cls_name(args[0]), name, pseudo_args))class Overriding: # <1> """a.k.a. data descriptor or enforced descriptor""" def __get__(self, instance, owner): print_args(‘get‘, self, instance, owner) def __set__(self, instance, value): print_args(‘set‘, self, instance, value)class OverridingNoGet: # <2> """an overriding descriptor without ``__get__``""" def __set__(self, instance, value): print_args(‘set‘, self, instance, value)class NonOverriding: # <3> """a.k.a. non-data or shadowable descriptor""" def __get__(self, instance, owner): print_args(‘get‘, self, instance, owner)class Managed: # <4> over = Overriding() over_no_get = OverridingNoGet() non_over = NonOverriding() def spam(self): # <5> print(‘-> Managed.spam({})‘.format(display(self)))
- 有__get__和__set__方法的典型覆蓋型描述符,在這個樣本中,各個方法都調用了print_args()函數
- 沒有__get__方法的覆蓋型描述符
- 沒有__set__方法,所以這是非覆蓋型描述符
- 託管類,使用各個描述符類的一個執行個體
- spam方法放這裡是為了對比,因為方法也是描述符
覆蓋型描述符
實現__set__方法的描述符屬於覆蓋型描述符,雖然描述符是類屬性,但是實現了__set__方法的話,會覆蓋對執行個體屬性的賦值操作。特性也是覆蓋型描述符,見Python動態屬性和特性(二),如果沒有提供設值函數,property類中的fset方法就會拋出AttributeError異常,指明那個屬性時唯讀
下面我們來看下覆蓋型描述符的行為:
>>> obj = Managed() # <1>>>> obj.over # <2>-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)>>> Managed.over # <3>-> Overriding.__get__(<Overriding object>, None, <class Managed>)>>> obj.over = 8 # <4>-> Overriding.__set__(<Overriding object>, <Managed object>, 8)>>> obj.over # <5>-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)>>> obj.__dict__["over"] = 9 # <6>>>> vars(obj) # <7>{‘over‘: 9}>>> obj.over # <8>-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
- 建立受管理的執行個體Managed對象
- obj.over觸發描述符的__get__方法,__get__方法第一個參數是描述符執行個體,第二個參數的值是受管理的執行個體obj,第三個參數是託管類對象
- Managed.over 觸發描述符的__get__方法,第二個參數(instance)的值是 None,第一個和第三個同上
- 為obj.over賦值,觸發描述符的__set__方法,最後一個參數的值是8
- 讀取 obj.over,仍會觸發描述符的__get__方法
- 跳過描述符,直接通過obj.__dict__屬性設值
- 確認值在obj.__dict__屬性中,在over鍵名下
- 然而,即使是名為over的執行個體屬性,Managed.over描述符仍會覆蓋讀取 obj.over 這個操作
沒有__get__方法的覆蓋型描述符
>>> obj.over_no_get # <1><descriptorkinds.OverridingNoGet object at 0x0000001742369780>>>> Managed.over_no_get # <2><descriptorkinds.OverridingNoGet object at 0x0000001742369780>>>> obj.over_no_get = 8 # <3>-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 8)>>> obj.over_no_get # <4><descriptorkinds.OverridingNoGet object at 0x0000001742369780>>>> obj.__dict__[‘over_no_get‘] = 6 # <5>>>> obj.over_no_get # <6>6>>> obj.over_no_get = 7 # <7>-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)>>> obj.over_no_get # <8>6
- 描述符執行個體沒有__get__方法,所以obj.over_no_get從類中擷取描述符執行個體
- 直接從託管類中讀取over_no_get屬性,也就是描述符執行個體
- 為obj.over_no_get賦值hui會觸發描述符類的__set__方法
- 因為__set__方法沒有修改屬性,所以在此讀取obj.over_no_get擷取的仍然是託管類中的描述符執行個體
- 通過執行個體的__dict__屬性設定名為over_no_get的執行個體屬性
- 現在,over_no_get執行個體屬性會覆蓋描述符執行個體
- 為obj.over_no_get執行個體屬性賦值,仍然會經過__set__方法
- 讀取時,只要有同名的執行個體屬性,描述符執行個體就會被覆蓋
非覆蓋型描述符:沒有實現__set__方法的描述符稱為是非覆蓋型描述符,如果設定了同名的執行個體屬性,描述符會被覆蓋,致使描述符無法處理那個執行個體的那個屬性
>>> obj.non_over # <1>-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)>>> obj.non_over = 6 # <2>>>> obj.non_over # <3>6>>> Managed.non_over # <4>-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)>>> del obj.non_over # <5>>>> obj.non_over # <6>-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)
- obj.non_over觸發描述符的__get__方法,第二個參數的值是obj
- Managed.non_over是非覆蓋型描述符,因此沒有幹涉賦值操作的__set__方法
- obj有個名為non_over的執行個體屬性,把Managed類的同名描述符屬性遮蓋掉
- Managed.non_over描述符依然存在,會通過類截獲這次訪問
- 刪除non_over執行個體屬性
- 讀取obj.non_over時,會觸發類中描述符的__get__方法
覆蓋類中的描述符:不管描述符是不是覆蓋型的,為類屬性賦值都能覆蓋描述符
>>> obj = Managed() # <1>>>> Managed.over = 1 # <2>>>> Managed.over_no_get = 2>>> Managed.non_over = 3>>> obj.over, obj.over_no_get, obj.non_over # <3>(1, 2, 3)
- 建立一個Managed執行個體
- 覆蓋類中的描述符屬性
- 通過執行個體訪問描述符屬性,新的值覆蓋描述符
方法是描述符:在類中定義的函數屬於Binder 方法,使用者定義的函數都有__get__方法,所以依附到類上時,就相當於描述符,方法是非覆蓋型描述符
>>> obj = Managed()>>> obj.spam # <1><bound method Managed.spam of <descriptorkinds.Managed object at 0x00000017423284A8>>>>> Managed.spam # <2><function Managed.spam at 0x0000001742322AE8>>>> obj.spam = 7 # <3>>>> obj.spam7
- obj.spam擷取的是Binder 方法對象
- Managed.spam擷取的是函數
- 如果為obj.spam賦值,會遮蓋類屬性,導致無法通過obj執行個體訪問spam方法
函數沒有__set__方法,因此是非覆蓋型描述符,從上面的例子來看,obj.spam和Managed.spam擷取的是不同對象。與描述符一樣,通過託管類訪問時,函數__get__方法會返回自身的引用,但是通過執行個體訪問時,函數的__get__方法返回的是Binder 方法對象:一種可調用的對象,裡麵包裝著函數,並把受管理的執行個體(如obj)綁定給函數的第一個參數(即self),這與functools.partial函數的行為一致
為了瞭解這種機制,讓我們看下面一個例子:
import collectionsclass Text(collections.UserString): def __repr__(self): return ‘Text({!r})‘.format(self.data) def reverse(self): return self[::-1]
測試Text類:
>>> word = Text("forward")>>> word # <1>Text(‘forward‘)>>> word.reverse() # <2>Text(‘drawrof‘)>>> Text.reverse(Text(‘backward‘)) # <3>Text(‘drawkcab‘)>>> type(Text.reverse), type(word.reverse) # <4>(<class ‘function‘>, <class ‘method‘>)>>> list(map(Text.reverse, [‘repaid‘, (10, 20, 30), Text(‘stressed‘)])) # <5>[‘diaper‘, (30, 20, 10), Text(‘desserts‘)]>>> func = Text.reverse.__get__(word) # <6>>>> func() # <7>Text(‘drawrof‘)>>> Text.reverse.__get__(None, Text) # <8><function Text.reverse at 0x000000266209E598>>>> Text.reverse<function Text.reverse at 0x000000266209E598>>>> word.reverse # <9><bound method Text.reverse of Text(‘forward‘)>>>> Text.reverse.__get__(word)<bound method Text.reverse of Text(‘forward‘)>>>> word.reverse.__self__ # <10>Text(‘forward‘)>>> word.reverse.__func__ is Text.reverse # <11>True
- Text執行個體的repr方法返回一個類似Text構造方法調用的字串,可用於建立相同的執行個體
- reverse方法返回反向拼字的單詞
- 在類上調用方法並傳入一個執行個體相當於調用執行個體的函數
- 從類擷取方法和從執行個體擷取方法的類型是不同的,一個是function,一個是method
- Text.reverse相當於函數,甚至可以處理Text執行個體之外的其他對象
- 函數都是非覆蓋型描述符。在函數上調用__get__方法時傳入執行個體,得到的是綁定到那個執行個體上的方法
- 我們執行第六個步驟得到的func對象,與在執行個體上調用函數效果一樣
- 調用函數的__get__方法時,如果instance 參數的值是None,那麼得到的是函數本身
- word.reverse運算式其實會調用Text.reverse.__get__(word),返回對應的Binder 方法
- Binder 方法對象有個__self__屬性,其值是調用這個方法的執行個體引用
- Binder 方法的__func__屬性是依附在託管類上那個原始函數的引用
Binder 方法對象還有個__call__方法,用於處理真正的調用過程,這個方法調用__func__屬性引用的原始函數,把函數的第一個參數設定為Binder 方法的__self__屬性,這就是形參self的隱式綁定方式
描述符用法建議:
- 使用特性以保持簡單:內建的property類建立的其實是覆蓋型描述符,__set__方法和__get__方法都實現了,即便不定義設值方法也是如此。特性的__set__方法預設拋出AttributeError: can‘t set attribute異常,因此建立唯讀屬性最簡單的方式是使用特性
- 唯讀描述符必須有__set__方法:如果使用描述符類實現唯讀屬性,__get__和__set__兩個方法都必須定義,否則執行個體的同名屬性會遮蓋住描述符,唯讀屬性的__set__方法只需要拋出AttributeError異常,並提供合適的錯誤資訊
- 用於驗證的描述符可以只有__set__方法:對僅用於驗證的描述符來說,__set__方法應該檢查value參數獲得的值,如果有效,使用描述符執行個體的名稱為鍵,直接在執行個體的__dict__ 屬性中設定。這樣,從執行個體中讀取同名屬性的速度很快,因為不用經過 __get__方法處理
- 僅有__get__方法的描述符可以實現高效緩衝:如果只編寫了__get__方法,那麼建立的是非覆蓋型描述符。這種描述符可用於執行某些耗費資源的計算,然後為執行個體設定同名屬性,緩衝結果。 同名執行個體屬性會遮蓋描述符,因此後續訪問會直接從執行個體的__dict__ 屬性中擷取值,而不會再觸發描述符的__get__方法
- 非特殊的方法可以被執行個體屬性遮蓋:由於函數和方法只實現了__get__方法,它們不會處理同名執行個體屬性的賦值操作。因此,像 my_obj.the_method = 7這樣簡單賦值之後, 後續通過該執行個體訪問the_method得到的是數字7——但是不影響類或其他執行個體。然而,特殊方法不受這個問題的影響。解譯器只會在類中尋找特殊的方法,也就是說,repr(x)執行的其實是x.__class__.__repr__(x),因此x的__repr__屬性對repr(x)方法調用沒有影響。出於同樣的原因,執行個體的_getattr__屬性不會破壞常規的屬性訪問規則
Python屬性描述符(二)