Python學習:描述符
一、描述符是什麼
描述符:是一個類,只要內部定義了方法__get__, __set__, __delete__中的一個或者多個。描述符,屬性,方法繫結等內部機制都是描述符在起作用。描述符以單個屬性出現,並針對該屬性的不同訪問行為做出響應。最重要的是,描述符能“感知”通過什麼引用該屬性,從而和目標建立繫結關聯。
二、描述符的實現
class Descriptor: """ 描述符 """ def __set_name__(self, owner, name): """ 描述符屬性必須定義為型別成員,所以其自身不適合儲存例項相關的狀態,在建立屬性時,__set_name__方法被呼叫,並可以通過引數獲知目標型別(owner),以及屬性名稱 :param owner: :param name: :return: """ print(self, owner, name) self.name = f"__{name}__" def __get__(self, instance, owner): """ 以型別或例項訪問描述符屬性時,__get__被自動呼叫,且會接收到型別和例項引用 :param instance: :param owner: :return: """ print(self,instance,owner) return getattr(instance,self.name, None) def __set__(self, instance, value): """ 僅在例項引用時被呼叫。以型別引用進行賦值,會導致描述符屬性被替換 :param instance: :param value: :return: """ print(self, instance, value) setattr(instance, self.name, value) def __delete__(self, instance): """ 僅在例項被引用時呼叫。以型別引用進行刪除操作,會導致描述符屬性被刪除 :param instance: :return: """ print(self, instance) raise AttributeError("delete is disabled") class X: data = Descriptor() x = X() # 執行__set_name__ <__main__.Descriptor object at 0x0000026DEB4E8438> <class '__main__.X'> data x.data = 100 # 執行__set__ <__main__.Descriptor object at 0x0000018A54408470> <__main__.X object at 0x0000018A54408908> 100 print(x.data) # 執行__get__ <__main__.Descriptor object at 0x0000020685688438> <__main__.X object at 0x00000206856888D0> <class '__main__.X'> print(x.__dict__) # {'__data__': 100} print(X.__dict__) # {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x000001E841598438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None} X.data = 2 # 以型別引用進行賦值,會導致描述符屬性被替換 print(x.data) # 2 print(X.data) # 2 print(x.__dict__) # {'__data__': 100} print(X.__dict__) # {'__module__': '__main__', 'data': 2, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}
三、資料描述符
如果定義了__set__或__delete__方法,那麼我們便稱其為資料描述符(data descriptor),而僅有__get__的則是非資料描述符(non-data descriptor)。這兩者的區別在於,資料描述符屬性的優先順序高於例項名字空間中的同名成員。
class Descriptors: """ 資料描述符 """ def __set_name__(self, owner, name): self.name = name # 獲取Descriptors 例項物件名字 def __get__(self, instance, owner): print("執行Descriptors的get") return self.name def __set__(self, instance, value): self.name = value print("執行Descriptors的set") def __delete__(self, instance): print("執行Descriptors的delete") class Light: # 使用描述符 name = Descriptors() def __init__(self, name, price): self.name = name self.price = price # 使用類的例項物件來測試 light = Light("電燈泡", 60) # 執行描述符的set內建屬性 light.name # 執行描述符的get內建屬性 print(light.__dict__) # 檢視例項的字典,不存在name {'price': 60} print(Light.__dict__) # 檢視類的字典,存在name(為描述符的物件) # {'__module__': '__main__', 'name': <__main__.Descriptors object at 0x000002261E7D8438>, # '__init__': <function Light.__init__ at 0x00000226257FED90>, '__dict__': <attribute '__dict__' of 'Light' objects>, # '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None} del light.name # 執行描述符的delete內建屬性 del Light.name # 以型別引用進行刪除操作,會導致描述符屬性被刪除 print(Light.__dict__) # {'__module__': '__main__', '__init__': <function Light.__init__ at 0x000001CBC197EEA0>, '__dict__': <attribute '__dict__' of 'Light' objects>, '__weakref__': <attribute '__weakref__' of 'Light' objects>, '__doc__': None} print(light.name) # 報錯,描述符屬性被刪除
如果註釋掉__set__,就成為了非資料描述符。
描述符的優先順序問題:類屬性>資料描述符>例項屬性>非資料描述符>找不到屬性觸發__getattr__()
說明問題一:類屬性>資料描述符
class Descriptor: def __get__(self, instance, owner): print("__get__") return self.name def __set__(self, instance, value): print("開始賦值:", value) self.name = value print("__set__") class X: data = Descriptor() x = X() x.data = 100 # 呼叫__set__賦值 print(x.__dict__) # {} x.data = 3 print(x.data) # 3 print(x.__dict__) # {} print(X.data) # 呼叫__get__ print(X.__dict__) # {'__module__': '__main__', 'data': <__main__.Descriptor object at 0x0000015EE4A38438>, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None} X.data = 44444 # 語句沒有觸發set的執行,說明類屬性的優先順序大於資料描述符的優先,此時相當於類屬性覆蓋了資料描述符,從而說明對類屬性的一切操作都與描述符無關 print(x.data) # 4 print(x.__dict__) # {} print(X.__dict__) # {'__module__': '__main__', 'data': 44444, '__dict__': <attribute '__dict__' of 'X' objects>, '__weakref__': <attribute '__weakref__' of 'X' objects>, '__doc__': None}
說明問題二:資料描述符>例項屬性 參考“描述符程式碼” ,資料描述符的優先順序大於例項屬性的優先順序,此時例項屬性name被資料描述符所覆蓋,而price沒有描述符代理,所以它任然是例項屬性。
說明問題三:例項屬性>非資料描述符
class Descriptors: """ 非資料描述符 """ def __get__(self, instance, owner): print("執行Descriptors的get") def __delete__(self, instance): print("執行Descriptors的delete") class X: data = Descriptors() x = X() x.data = 3 # 報錯,AttributeError: __set__
四、方法繫結
因為函式預設實現了描述符協議,所以當以例項或型別訪問方法時,__get__首先被呼叫。型別和例項作為引數被傳入__get__,從而截獲繫結目標(self),如此就將函式包裝稱繫結方法物件返回。實際被執行的,就是這個會隱式傳入第一個引數的包裝品。
class Person: def __init__(self, name, age): self.name = name self.age = age def print_info(self): print("my name is %s ,my age is %s" % (self.name, self.age)) p = Person("ways", 13) print(p.print_info) # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>> print(p.print_info.__get__(p,Person)) # <bound method Person.print_info of <__main__.Person object at 0x000002092DBE8438>> m = p.print_info.__get__(p,Person) Person.print_info(m.__self__,) # my name is ways ,my age is 13 print(m.__self__, m.__func__) # <__main__.Person object at 0x000002092DBE8438> <function Person.print_info at 0x0000020934BEED08> """ 方法執行分成了兩個步驟: p.print_info(): #1. m = p.print_info.__get__(p,Person) 將函式包裝成繫結方法 #2. m()等價Person.print_info(m.__self__,) 執行時,隱式將self/cls引數傳遞給目標函式 """
五、描述符的使用例子
1、模擬property
class My_Property: """ 使用描述符模擬property """ def __init__(self, func): self.func = func print(self.func) # <function Test.my_area at 0x000001671607ED90> def __get__(self, instance, owner): res = self.func(instance) # 回撥傳入的函式,將執行結果儲存在res中 setattr(instance,self.func.__name__,res) # 為函式名func.__name__ 設定值為res,存入物件的字典 return res class Test: def __init__(self,weight,height): self.weight = weight self.height = height @My_Property def my_area(self): return self.weight*self.height test = Test(3, 4) print(test.my_area) # 12 print(test.__dict__) # {'weight': 3, 'height': 4, 'my_area': 12}
六、描述符的使用總結
1、描述符是可以實現大部分python類特性中的底層魔法,包括@classmethod,@staticmethd,@property甚至是__slots__屬性
2、描述父是很多高階庫和框架的重要工具之一,描述符通常是使用到裝飾器或者元類的大型框架中的一個元件.
參考:https://www.cnblogs.com/Lynnblog/p/9033455.html 和《python3學習筆記上》