• 27


    1 描述器

            一般来说,一个描述器是一个有绑定行为的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。

    有这些方法的对象叫做描述器。

            默认对属性的访问控制是从对象的字典里面(__dict__)中获取、设置和删除它。举例来说, 比如 a.x 的查找顺序是, a.__dict__['x'] , 然后 type(a).__dict__['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)). 如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)。

    2 描述器协议

    描述器主要涉及三个方法:

    1. descr.__get__(self, obj, type=None) --> value
    2. descr.__set__(self, obj, value) --> None
    3. descr.__delete__(self, obj) --> None

    一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找、设置和删除行为。

    2.1 非数据描述器

    在类中仅仅定义了__get__方法的描述器被称为非数据描述器(non-data descriptor)。

    非数据描述器的优先级低于实例的__dict__。

    class A:
        def __init__(self):
            self.a1 = 'a1'
            print('A.init')
    
        def __get__(self, instance, owner):
            pass
            # return self
    class B:
        x = A()
    
        def __init__(self):
            print('B.init')
    
    print('-' * 20)
    b = B()
    print(b.x.a1)
    
    
    # Traceback (most recent call last):
    
    # A.init
    
    # --------------------
    
    # B.init
    
    #   File "E:/Python - base - code/ClassDesc.py", line 20, in <module>
    
    #     print(b.x.a1)
    
    # AttributeError: 'NoneType' object has no attribute 'a1'
    

    分析:

    1. Class A实现了__get__方法,所以它是一个非数据描述器
    2. 由于Class B里面设置的x属性是Class A的实例,所以在定义阶段就会实例化,把实例化的对象赋给x属性,所以会先执行A的__init__方法。
    3. 访问实例b的x属性时,发现值是一个描述器,然后就会被描述器A的__get__方法捕获
    4. __get__方法默认然会None,所以None对象没有a1属性,所以报属性错误
    5. 在__get__方法中,return self就可以访问了

    那么self是什么,__get__方法的参数都是什么意思:

    • self:对应A的实例(这里是属性x)
    • owner:对应的是x属性的拥有者,也就是B类
    • instance:它的值有两个
      • 当使用owner类直接调用时,它是None
      • 当使用owner类的实例调用是,是实例本身

    2.1.1 实例分析

    下面是小例子,分析代码结果

    class Person:
    
        def __init__(self):
            self.country = 'Earth'
    
        def __get__(self, instance, owner):
            return self.country
    
    class ChinaPeople:
        country = Person()
    
        def __init__(self,name,country):
            self.name = name
            self.country = country
    
    daxin = ChinaPeople('daxin','China')
    print(daxin.country)
    

    分析:

    1. 由于描述器Person是一个非数据描述器,优先级低于实例自己的__dict__
    2. 实例在初始化时对self.country属性赋值,会直接在自己的__dict__中,写入{"country":"China}
    3. 在访问时根据MRO,优先访问实例自己的__dict__,所以结果是China

    2.2 数据描述器

    同时实现了__get__、__set__方法就称为数据描述器(data descriptor)

    数据描述器的优先级高于实例的字典__dict__。

    class A:
    
        def __init__(self):
            self.name = 'A'
    
        def __get__(self, instance, owner):
            print('From A __get__')
            return self.name
    
        def __set__(self, instance, value):
            print('From A __set__')
    
    class B:
        name = A()
        def __init__(self):
            self.name = 'B'
    
    b = B()
    print(b.name)
    
    
    # 结果:
    
    # From A __set__
    
    # From A __get__
    
    # A
    

    分析:

    1. 描述器A,实现一个数据描述器,优先级高于实例的自己的__dict__
    2. 在对b进行实例化的时候,设置了b的name属性,根据mro规则,找到父类B的name属性,然后发现其是一个数据描述器,然后被描述器A的__set__方法捕获。
    3. 当打印实例属性name时,由于数据描述器中,没有对传入的'B'进行赋值,所以这里'B'就丢了,最后访问属性name,会被描述器的__get__方法捕获,并返回描述器的name属性,所以打印是"A"

    那么self是什么,__set__方法的参数都是什么意思:

    • self:对应A的实例(这里是属性name)
    • instance:对应的是实例本身,这里就是b
    • value:表示设置的值(这里就是'B')

    2.2.1 实例

    分析下面代码的运行原理

    class A:
    
        def __init__(self):
            self.name = 'A'
    
        def __get__(self, instance, owner):
            print('From A __get__')
            # return self.name
            return instance.__dict__['name']
    
        def __set__(self, instance, value):
            print('From A __set__')
            instance.__dict__['name'] = value
    
    class B:
        name = A()
    
        def __init__(self):
            self.name = 'B'
    
    b = B()
    print(b.name)
    

    分析:

    1. 当b在初始化时,对name属性进行了设置,所以第一步先按照mro查找name属性。
    2. 在父类B中,查找到类属性name,它的结果是一个数据描述器,所以设置的请求被数据描述器的__set__方法捕获,在__set__方法中,为实例自己的__dict__注入了属性name以及它的值。
    3. 在打印name属性时,由于数据描述器的优先级高于实例的__dict__,所以操作被描述器的__get__方法捕获,在内部返回了实例自己__dict__的属性name,所以最后打印'B'

    2.3 描述器的调用及属性访问顺序

            当类中存在描述器时,那么对象属性的调用就会发生变化。根据上面的例子,我们知道,实例属性访问的优先级为:数据描述器 > 实例字典__dict__ > 非数据描述器

    特别注意:这里的访问顺序指的是:实例属性对应一个描述器时的顺序。,如果直接对类属性进行赋值操作,会直接覆盖类的描述器。
    结合前面学的魔术方法,分析整个过程。

    1. 实例daxix的属性name(daxin.name) 本质上执行的是daxin.__getattribute__()方法
    2. daxin.__getattribute__() 其实是type(daxin).__dict__['name'].__get__(daxin,type(daxin))

    使用Pyhon描述这个过程就是

    def __getattribute__(self, key):
        print('from B __getattribute__')
        v = super(B, self).__getattribute__(key)   # 这里用 self.__getattribute__就会递归了
        # v = object.__getattribute__(self, key)   # 使用super的方法,等同于直接调用object
        if hasattr(v, '__get__'):
            return v.__get__(self, type(self))  
        return v
    

    完整的代码:

    class A:
    
        def __init__(self):
            self.name = 'A'
    
        def __get__(self, instance, owner):
            print('From A __get__')
            # return self.name
            return instance.__dict__['name']
    
        def __set__(self, instance, value):
            print('From A __set__')
            instance.__dict__['name'] = value
    
    class B:
        name = A()
    
        def __init__(self):
            self.name = 'B'
    
        def __getattribute__(self, key):
            print('from B __getattribute__')
            v = super(B, self).__getattribute__(key)   
            if hasattr(v, '__get__'):
                return v.__get__(self, type(self))
            return v
    
    b = B()
    print(b.name)
    

    2.4 描述器总结

    总结几点比较重要的:

    • 描述器的调用是因为__getattribute__()方法
    • 重写__getattribute__()方法会组织正常的描述器调用
    • __getattribute__()只对新式类的实例可用
    • object.__getattribute__()和type.__getattribute__()对__get__()的调用不一样
    • 数据描述器总是比实例字典优先
    • 非数据描述器可能被实例字典重写/覆盖(非数据描述器不如实例字典优先)

    3 Python的描述器体现

            描述器在Python中应用非常广泛。我们定义的实例方法,包括类方法(classmethod)和静态方法(staticmethod)都属于非数据描述器。所以实例可以重新定义和覆盖方法。这样就可以使一个实例拥有与其他实例不同的行为(方法重写)。
            但property装饰器不然,它是一个数据描述器,所以实例不能覆盖属性。

    class A:
        def __init__(self,name ):
            self._name = name
    
        @staticmethod 
        def hello():         # 非数据描述器
            print('world')
    
        @classmethod
        def world(cls):      # 非数据描述器
            print('world')
    
        @property
        def name(self):      # 数据描述器
            return self._name
    
        def welcome(self):   # 非数据描述器
            print('Welcome')
    
    class B(A):            
        def __init__(self,name):
            super().__init__(name)
    
    
    daxin = B('daxin')
    daxin.hello = lambda : print('modify hello')  # 可以被覆盖
    daxin.world = lambda : print('modify world')  # 可以被覆盖
    daxin.welcome = lambda : print('modify welcome')  # 可以被覆盖
    daxin.name = lambda self: self._name  # 无法被覆盖
    
    daxin.hello()
    daxin.world()
    daxin.welcome()
    

    3.1 staticmethod简单实现

    下面是一个简单的StaticMethod的实现

    class StaticMethod:
        def __init__(self, fn):
            self.fn = fn
    
        def __get__(self, instance, owner):
            return self.fn
    
    class A:
    
        @StaticMethod  # hello = StaticMethod(hello)
        def hello():  
            print('hello world')
    
    daxin = A()
    daxin.hello()  # hello() = StaticMethod().fn()
    

    静态方法不需要传参,那么只需要在__get__方法拦截后,仅仅返回方法本身即可。

    3.2 ClassMethod简单实现

    import  functools
    class ClassMethod:
    
        def __init__(self, fn):
            self.fn = fn
    
        def __get__(self, instance, owner):
            #return lambda : self.fn(owner)
            return  functools.partial(self.fn,owner)
    
    class A:
    
        @ClassMethod
        def hello(cls):
            print('hello world {}'.format(cls.__name__))
    
    daxin = A()
    daxin.hello()  # hello() = functools.partial(self.fn,owner)
    

    类方法由于默认会把类当作参数传递,所以需要把方法的第一个参数固定为类,所以使用偏函数来固定,是一个比较好的办法,又或者使用lambda,由于lambda函数只能接受一个参数,所以当类方法是多个参数时,无法接受。

    3.3 对实例的数据进行校验

    现有如下代码:

    class Person:
        def __init__(self,name:str, age:int):
            self.name = name 
            self.age = age 
    

    对上面类的属性name,age进行数据类型的校验。
    思路:

    1. 写函数,直接在__init__中检查,如果不符合直接抛出异常(一般人都会)
    2. 装饰器(多数人会)
    3. 描述器(少数人会)
    4. 描述器版装饰器(基本没人会)

    3.3.1 直接在__init__函数中检查

    class Person:
        def __init__(self, name:str, age:int):
            # 每次都判断,然后赋值
            # if self._typecheck(name,str):
            #     self.name = name
            # if self._typecheck(age, int):
            #     self.age = age
    
            # 或者直接构建需要的数据类型,一次性判断,最后赋值
            params = [(name,str),(age,int)]
            for param in params:
                if not self._typecheck(*param):
                    raise TypeError(param[0])
            self.name = name
            self.age = age
    
        def _typecheck(self,value,typ):
            if not isinstance(value, typ):
                raise TypeError(value)
            return True
    
    daxin = Person('daxin',20)
    print(daxin.name)
    print(daxin.age)
    

    看起来也太丑了,不能复用不说,在初始化阶段还做了大量的逻辑判断,也不容易让别人明白你真正的意图是啥。

    3.3.2 装饰器版本

    import inspect
    
    def TypeCheck(cls:object):
        def wrapper(*args,**kwargs):
            sig = inspect.signature(cls)  # 获取签名对象
            param = sig.parameters.values()  # 抽取签名信息(有序)
            data = zip(args,param)  # 构建值与类型的元组
            for value,typ in data:
                if typ.annotation != inspect._empty:   # 当定义了参数注解时,开始参数判断
                    if not isinstance(value,typ.annotation):
                        raise TypeError(value)   # 判断不通过,爆出异常
            return cls(*args,**kwargs)
        return wrapper
    
    @TypeCheck  # Person = TypeCheck(Person)('daxin',20)  ==> wrapper('daxin',20)
    class Person:
        def __init__(self,name:str, age:int):
            self.name = name 
            self.age = age
    
    daxin = Person('daxin','20')
    print(daxin.name)
    print(daxin.age)
    

    看起来很好的解决了参数类型的检查,并且也可以针对不同类继续进行参数检查,所以说:装饰器真香

    3.3.3 描述器版本

    class TypeCheck:
        def __init__(self, name, typ):
            self.name = name
            self.typ = typ
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            if not isinstance(value,self.typ):
                raise TypeError(value)
            instance.__dict__[self.name] = value
    
    class Person:
    
        name = TypeCheck('name',str)      # 硬编码
        age = TypeCheck('age',int)        # 硬编码
    
        def __init__(self, name:str, age:int):
            self.name = name
            self.age = age
    
    daxin = Person('daxin','20')
    print(daxin.name)
    print(daxin.age)
    

    3.3.4 装饰器+描述器版本之函数装饰器

    import inspect
    
    
    class TypeCheck:
    
        def __init__(self, name, typ):
            self.name = name
            self.typ = typ
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            if not isinstance(value,self.typ):
                raise TypeError(value)
            instance.__dict__[self.name] = value
    
    
    # 动态注入name,age描述器属性
    def AttriCheck(cls:object):
        def wrapper(*args,**kwargs):
            sig = inspect.signature(cls)
            params = sig.parameters
            for k,v in params.items():
                print(v.annotation)
                if v.annotation != inspect._empty:
                    if not hasattr(cls,k):
                        setattr(cls,k,TypeCheck(k,v.annotation))
            return cls(*args,**kwargs)
        return wrapper
    
    @AttriCheck   # Person = AttriCheck(Person)
    class Person:
    
        def __init__(self, name: str, age: int):
            self.name = name
            self.age = age
    
    a = Person('daxin', 20)
    print(a.name)
    print(a.age)
    

    使用装饰器结合描述器时,类必须包含对应同名描述器,才可以利用描述器进行参数检查,所以,利用反射,将参数注入类中,然后通过描述器进行检查

    3.3.5 装饰器+描述器版本之类装饰器

    能否把上面的装饰器函数,改为类?

    import inspect
    
    class TypeCheck:
    
        def __init__(self, name, typ):
            self.name = name
            self.typ = typ
    
        def __get__(self, instance, owner):
            return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            if not isinstance(value,self.typ):
                raise TypeError(value)
            instance.__dict__[self.name] = value
    
    class AttriCheck:
        def __init__(self,cls):
            self.cls = cls
    
        def __call__(self, *args, **kwargs):
            sig = inspect.signature(self.cls)
            params = sig.parameters
            for name,typ in params.items():
                if typ.annotation != inspect._empty:
                    if not hasattr(self.cls, name):
                        setattr(self.cls,name,TypeCheck(name,typ.annotation))
            return self.cls(*args,**kwargs)
    
    
    @AttriCheck   # Person = AttriCheck(Person)
    class Person:
    
        def __init__(self, name: str, age: int):
            self.name = name
            self.age = age
    
    a = Person('daxin', '20')
    print(a.name)
    print(a.age)
    
    

    4 疑问

    看下面例子:

    class B:
        def __init__(self, data):
            self.data = data
    
        def __get__(self, instance, owner):
            return self.data
    
        def __set__(self, instance, value):
            self.data = value
    
    
    class C:
        name = B('daxin')
        age = B(20)
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    daxin = C('tom',18)
    dachenzi = C('Jack',29)
    print(daxin.name) 
    

    结果是'Jack',为什么呢?

    1. name和age属于类属性,只会在定义的时候实例化一次!不同实例的name和age属性是公用的!
    2. 在描述器中,把实例设置的值,绑定到了描述器本身的属性上去了。
    3. 不同实例的name和age属性都指向了相同的描述器,并且每次修改的都是同一个属性。
    4. 这种坑是要避免的,尽量把属性绑定在实例自己身上
  • 相关阅读:
    Maven学习总结(9)——使用Nexus搭建Maven私服
    Maven学习总结(8)——使用Maven构建多模块项目
    Maven学习总结(8)——使用Maven构建多模块项目
    Maven学习总结(8)——使用Maven构建多模块项目
    Maven学习总结(7)——eclipse中使用Maven创建Web项目
    Maven学习总结(7)——eclipse中使用Maven创建Web项目
    Maven学习总结(7)——eclipse中使用Maven创建Web项目
    Maven学习总结(6)——Maven与Eclipse整合
    Maven学习总结(6)——Maven与Eclipse整合
    nmon
  • 原文地址:https://www.cnblogs.com/dachenzi/p/10505515.html
Copyright © 2020-2023  润新知