python高級之描述器

  • 描述器用到的方法

    用到3個魔術方法: __get__()、__set__()、__delete__()
    方法使用格式:
        obj.__get__(self, instance, owner)
        obj.__set__(self, instance, value)
        obj.__delete__(self, instance)

    self: 指當前類的實例本身
    instance: 指owner的實例
    owner: 指當前實例作爲屬性的所屬類

代碼一

以下代碼執行過程:
    定義B類時,執行A()賦值操作,進行A類的初始化,再打印B類調用類屬性x的a1屬性
    緊接着執行B類的初始化,通過b實例調用類屬性的x的a1屬性

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

class B:
    x  = A()
    def __init__(self):
        print('B.init')

print('-' * 20)
print(B.x.a1)

print('*' * 20)
b = B()
print(b.x.a1)


  • 描述器定義

Python中,一個類中實現了__get__、__set__、__delete__三個方法中的任何一個方法,
那麼這個類就是描述器.
如果僅實現了__get__,就是非數據描述符 non-data descriptor
同時實現了除__get__以外的__set__或__delete__方法,就是數據描述符 data descriptor

如果一個類的類屬性設置爲描述器,那麼它被稱爲此描述器的owner屬主

描述器方法何時被觸發:
    當屬主類中對是描述器的類屬性進行訪問時(即類似b.x),__get__方法被觸發
    當屬主類中對是描述器的實例屬性通過'.'號賦值時,__set__方法被觸發

代碼二

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print('A.__get__ {} {} {}'.format(self,instance,owner))

class B:
    x  = A()
    def __init__(self):
        print('B.init')

print('-' * 20)
print(B.x)
# print(B.x.a1)   # AttributeError B.x爲None,None沒有a1屬性

print('*' * 20)
b = B()
# print(b.x.a1)  # AttributeError B.x爲None,None沒有a1屬性

調用B類的類屬性,被A類__get__方法攔截,並返回值None


代碼三

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print('A.__get__ {} {} {}'.format(self,instance,owner))
        return self

class B:
    x  = A()
    def __init__(self):
        print('B.init')

print('-' * 20)
print(B.x)
print(B.x.a1)   

print('*' * 20)
b = B()
print(b.x)
print(b.x.a1)  

解決上例中的返回值爲None,將A類的實例返回,可成功調用A實例的a1屬性

代碼四

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print('A.__get__ {} {} {}'.format(self,instance,owner))
        return self

class B:
    x  = A()
    print(x)
    def __init__(self):
        print('B.init')
#         self.x = 100    #  實例調用x屬性時,直接查實例自己的__dict__
        self.x = A()      # 實例調用x屬性時,不進入A類的__get__方法
        print(self.x)      

print('-' * 20)
print(B.x)    # __get__
print(B.x.a1)    # __get__

print('*' * 20)
b = B()
print(b.x)
print(b.x.a1) 

總結: 不論是實例還是類,只要是訪問了是描述器的類屬性,
都會被描述器的__get__方法攔截


  • 屬性的訪問順序(本質)

代碼五

class A:
    def __init__(self):
        self.a1 = 'a1'
        print('A.init')

    def __get__(self,instance,owner):
        print('A.__get__ {} {} {}'.format(self,instance,owner))
        return self

    def __set__(self,instance,value):
        print('A.__set__ {} {} {}'.format(self,instance,value))

class B:
    x  = A()
    print(x)
    def __init__(self):
        print('B.init')
        self.x = 100
#         self.x = A()   # 同上面100結果類似
        print(self.x)

# print('-' * 20)
# print(B.x)
# print(B.x.a1)   

# print('*' * 20)
b = B()
# print(b.x)
# print(b.x.a1)  
print(b.__dict__)
print(B.__dict__)

屏蔽A類的__set__方法,實例的__dict__爲{'x': 100}
不屏蔽A類的__set__方法,實例的__dict__爲{}
__set__方法本質將實例的__dict__的屬性名清空,從而達到數據描述器優先於查實例字典的假象
  • Python中的描述器

描述器在Python中應用非常廣泛

Python的方法(包括staticmethod()和classmethod()) 都實現爲非數據描述器.
因此,實例可以通過'.'號進行生成屬性.
property()函數實現爲一個數據描述器.則實例不能使用'.'號進行賦值屬性.

示例

class A:
    @classmethod
    def foo(cls):
        pass

    @staticmethod
    def bar():
        pass

    @property
    def z(self):
        return 5

    def __init__(self): # 非數據描述器
        self.foo = 100
        self.bar = 200
#         self.z = 300    # z屬性不能使用實例覆蓋
a = A()
print(a.__dict__)
print(A.__dict__)


  • 練習


    • 實現StaticMethod裝飾器,完成staticmethod裝飾器的功能

  • class StaticMethod:
        def __init__(self,fn):
            self._fn = fn
    
        def __get__(self,instance,owner):
            print(self,instance,owner)
            return self._fn
    
    class A:
        @StaticMethod      # stmd = StaticMehtod(stmd)
        def stmd():
            print('stmd')
    
    print(A.__dict__)
    A.stmd()    # 類調用stmd屬性

    • 實現ClassMethod裝飾器,完成classmethod裝飾器的功能

  • from functools import partial
    
    
    class ClassMethod:
        def __init__(self,fn):
            self._fn = fn
    
        def __get__(self,instance,owner):
            print(self,instance,owner)
            return partial(self._fn,owner)      
            # 使用partial函數將類給作爲默認參數
    
    class A:
        @ClassMethod       # clsmd = ClassMethod(clsmd)
        def clsmd(cls):
            print('cls',cls.__name__)
    
    print(A.__dict__)
    A.clsmd()

    • 類初始化的參數檢查

  • import inspect
    
    class Typed:
    
        def __init__(self,tp):
            self._tp = tp
    
        def __get__(self,instance,owner):
            pass
    
        def __set__(self,instance,value):
            if not isinstance(value,self._tp):
                raise ValueError(value)
            setattr(instance.__class__,self._name,value)
    
    def pcheck(cls):
        def wrapper(*args):
            sig = inspect.signature(cls)
            params = sig.parameters
            for i,(name,param) in enumerate(params.items()):
                if param.empty != param.annotation:
    #                 if not isinstance(args[i],param.annotation):
    #                     raise ValueError(args[i])
                    setattr(cls,name,Typed(param.annotation))
            return cls(*args)
        return wrapper
    
    @pcheck
    class A:
    #     a = Typed(str)
    #     b = Typed(int)
        def __init__(self,a:str,b:int):
            self.a = a
            self.b = b
    
    A('1',2)

    描述器結合裝飾實現

    import inspect
    
    
    class Typed:
        def __init__(self,name,tp):
            self._name = name
            self._tp = tp
    
        def __get__(self,instance,owner):
            print('get',self,instance,owner)
            return instance.__dict__[self._name]
    
        def __set__(self,instance,value):
            print('set',self,instance,value)
            if not isinstance(value,self._tp):
                raise ValueError(value)
            instance.__dict__[self._name] = value
    
    class A:
        a = Typed('a',str)
        b = Typed('b',int)
        def __init__(self,a:str,b:int):
            self.a = a
            self.b = b
    
    a = A('1',2)
    print(a.__dict__)
    # print(type(a.a),type(a.b))
    print(a.a)

    描述器實現

    import inspect
    
    def pcheck(cls):
        def wrapper(*args):
            sig = inspect.signature(cls)
            params = sig.parameters
            for i,(_,param) in enumerate(params.items()):
                if param.empty != param.annotation:
                    if not isinstance(args[i],param.annotation):
                        raise ValueError(args[i])
            return cls(*args)
        return wrapper
    
    @pcheck    # A = pcheck(A)
    class A:
        def __init__(self,a:str,b:int):
            self.a = a
            self.b = b
    
    A('1','2')

    裝飾器版本

    class A:
        def __init__(self,a:str,b:int):
            if not (isinstance(a,str) and isinstance(b,int)):
                raise ValueError(a,b)
            else:
                self.a = a
                self.b = b
    A('1',2)

    直接參數檢查

    思路:
        實現參數檢查的本質是判斷傳入的參數是否符合形參定義的類型,也就是用isinstance進行判斷.
        因此參數檢查的不同實現的區別在於在哪些地方攔截傳入的參數,來進行檢查.
        上述實現的攔截地方:
            在類初始化時,在對實例屬性賦值之前攔截
            使用裝飾器,和inspect模塊,在實例化之前進行參數檢查
            使用描述器,在初始化時對實例屬性設置時,觸發描述器的__set__方法,在__set__方法中進行參數檢查,再對其實例的類添加類屬性
                (如果添加在實例上,則會遞歸調用回到__set__方法)
            使用裝飾器獲取參數註解,給類添加有描述器的類屬性,再通過描述器的方式進行參數檢查


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章