描述器由一個類對象定義,實現了__get__
方法,__set__
, __delete__
方法的類對象叫作描述器類對象,咱們指的描述器是指這個類的實例對象。python
描述器對象可以實現了兩個類的交互做用,將其中的一個類操做本身屬性的行爲轉而映射到另外一個類的一個方法上,實現更多靈活的操做。ide
class A: # 這是一個描述器類 def __get__(self, instance, owner): pass def __set__(self, instance, value): pass def __delete__(self, instance): pass class B: x = A() def __init__(self): self.x = 123 # 使用B類直接調用x 方法 B.x # 本應該返回A(),可是因爲A()是一個描述器對象,將會自動調用A()的__get__方法獲取返回值 # 使用B類實例調用 b = B() b.x # 因爲A是數據描述器,實例化中的x 和 類中的 x 屬性同名,self.x = 123, 將會觸發轉而調用A類中的`__set__`方法,instance爲B類的self實例,value爲值123
A 類是一個描述器類,當A的實例做爲其餘對象的的一個屬性時,其餘對象對該屬性進行操做時,將會調用這個描述類中對應操做的__get__
, __set__
, __delete__
方法。函數
非數據描述器是指只實現了 __get__
方法的描述器,類屬性名將不會影響實例屬性的賦值操做,當實例屬性和類屬性同名時候,實例依然優先訪問本身的屬性值.測試
class A: # 這是一個描述器類 def __get__(self, instance, owner): pass class B: x = A() # x 和 y 是兩個不一樣的實例描述器實例對象 y = A() def __init__(self): self.x = 123 # A是數據描述器,該語句正常執行 b = B() b.x # 123優先訪問本身的x屬性, 返回值 123 b.y # 因爲b沒有y屬性,調用B的類屬性y,便調用了A類中__get__函數獲取返回值 # 同時參數instance爲b,owner爲b的類,即B B.x # 調用A中的__get__,因爲是B類調用x,instance參數爲None,owner爲B
數據描述器在實現了__set__
方法的基礎上,還實現了__set__
或__delete__
方法其中的至少一個。當類屬性關聯一個屬性描述器時,經過實例訪問與描述器同名的屬性時候,仍然會觸發數據描述器,轉而調用描述器中的__get__
, __set__
,__delete__
方法,實例對象本身的屬性將不能直接訪問。編碼
class A: # 數據描述器類 def __get__(self, instance, owner): print("get") def __set__(self, instance, value): print("set") class B: x = A() def __init__(self): self.x = 123 b = B() print(b.x) ----- 執行結果分析 ------- b = B() 初始化一個B實例,調用__init__初始化方法,執行self.x = 123, 因爲B類的 x屬性是一個數據描述器,實例對 x 屬性的訪問仍然會被描述器攔截,self.x = 123 將會轉而調用A類中的__set__方法(由於這是賦值操做調用__set__,訪問操做調用__get__),將會打印__set__方法中的"set" print(b.x) 經過b.x 訪問x屬性時一樣被描述器攔截,對應調用描述器__get__方法,打印__get__中的"get",並返回None值,故print(b.x)打印None
Note:在使用反射函數setattr(b, "x", 123)
時,效果如同b.x = 123
,將會調用描述器,因此在描述器中不要出現instance.x = 123
相似的使用實例訪問 x 屬性的操做,不然將再次出發描述器,進而產生遞歸。 在描述器想實現對實例屬性的增長或者訪問,應該操做該實例屬性字典來避免遞歸現象code
class A: # 數據描述器類 def __init__(self, args): self.args = args def __get__(self, instance, owner): return instance.__dict__(self.args) def __set__(self, instance, value): instance.__dict__(self.args) = value class B: x = A("x") def __init__(self): self.x = 123
上面的程序雖然調用了描述器,可是描述器中的操做和普通賦值取值操做一致,在外部使用時感受不到描述器的存在。這樣咱們就能夠這些屬性進行賦值時對參數進行一些限制了。例如實現一個參數的類型檢測功能。orm
import inspect class A: # 數據描述器類 def __init__(self, args, typ): self.args = args self.typ = typ def __get__(self, instance, owner): return instance.__dict__(self.args) def __set__(self, instance, value): # 在賦值前對參數進行檢測,知足條件才添加到字典中 if isinstance(value, self.typ): instance.__dict__(self.args) = value else: raise TypeError("'{}' need the type of {}".format(self.args, self.typ)) def get_type(cls): sig = inspect.signature(cls) for name, parmas_obj in sig.parameters.items(): if params.annotation is not sig.empty: # 定義了參數註解纔會進行檢測 class B: x = A("x", int) def __init__(self, x:int): self.x = x # 賦值調用描述器
上面編碼是實現了對 x
參數的類型檢查,在描述器中__set__
方法中實現了參數的類型檢查,這樣即便在之後對實例的x
屬性進行從新賦值,仍然會再次檢查新賦值的類型。對象
上面代碼採用硬編碼對x屬性進行檢查,可使用一個裝飾器對B類動態添加須要檢測的屬性。遞歸
class TypCheck: def __init__(self, name, typ): self.name = name self.typ = typ def __get__(self, instance, owner): if instance is None: # 經過類名調用描述器屬性時 instance爲None值 raise TypeError("類名沒法調用該方法,只支持實例調用") return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, self.typ): raise TypeError("{} {}".format(self.name, self.typ)) instance.__dict__[self.name] = value def inject(cls): # 裝飾器,爲A類動態注入相似於上例中x = A(x, int)類型檢查的描述器 sig = inspect.signature(cls) for name, typ in sig.parameters.items(): if typ.annotation is not sig.empty: # cls.__dict__[name] = Typ_check(name, typ.annotation) setattr(cls, name, TypCheck(name, typ.annotation)) print(cls.__dict__) return cls @Inject class A: def __init__(self, name: str, age: int): self.name = name self.age = age # 簡單測試 a = A("name", 10) print(a.name) print(a.age) # 當參數類型不匹配時 a.name = 12 # TypeError a.age = "12" # TypeError