• 流畅的python——20 属性描述符


    二十、属性描述符

    实现了 __get____set____delete__ 方法的类是描述符。

    描述符的用法是,创建一个实例,作为另一个类的类属性。

    描述符类:实现描述符协议的类。

    托管类:把描述符实例类属性的类。

    描述符实例:描述符类的各个实例,声明为托管类的类属性。

    In [22]: class Q:  # 基于描述符协议实现。
        ...:     def __init__(self,storage_name):
        ...:         self.storage_name = storage_name  # 托管实例中存储值的属性名称
                 # 尝试为托管属性赋值时,会调用 __set__ 方法。
                 # self 是描述符实例,即 L.weight 或 L.price
                 # instance 是托管实例 即 L的实例,value 是要设定的值
        ...:     def __set__(self, instance, value):
        ...:         if value > 0:
        ...:             instance.__dict__[self.storage_name] = value  # 防止无限递归
        ...:         else:
        ...:             raise ValueError('value must be > 0')
                 # 读值不需要特殊处理,所以不需要自定义 __get__ 方法。
    
    In [23]: class L:
        ...:     weight = Q('weight')
        ...:     price = Q('price')
        ...:     def __init__(self, des, weight, price):
        ...:         self.des = des
        ...:         self.weight = weight
        ...:         self.price = price
        ...:     def subtotal(self):
        ...:         return self.weight * self.price
    

    应该把具体值存储在每个托管对象中,而不是描述符对象中

    为了理解错误的原因,可以想想 __set__ 方法前两个参数(self 和 instance)的意思。这里,self 是描述符实例,它其实是托管类的类属性。同一时刻,内存中可能有几千个 LineItem 实例,不过只会有两个描述符实例:LineItem.weight 和 LineItem.price。因此,存储在描述符实例中的数据,其实会变成 LineItem 类的类属性,从而由全部 LineItem 实例共享。

    问题:需要重复输入托管属性的名称,如果这样写就好了:但是,不太行。(其实我觉得重复写也挺好,,,)

    class LineItem:
     weight = Quantity()
     price = Quantity()
    

    自动获取存储属性的名称

    为了生成 storage_name,我们以 _Quantity# 为前缀,然后在后面拼接一个整数: Quantity.__counter 类属性的当前值,每次把一个新的 Quantity 描述符实例依附到类上,都会递增这个值。在前缀中使用井号能避免 storage_name 与用户使用点号创建的属性冲突,因为 nutmeg._Quantity#0 是无效的 Python 句法。但是,内置的 getattr和 setattr 函数可以使用这种“无效的”标识符获取和设置属性,此外也可以直接处理实例属性 __dict__

    In [25]: class QQ:
        ...:     __counter = 0  # 统计 QQ 描述符数量
        ...:     def __init__(self):
        ...:         cls = self.__class__  # cls是 QQ 类的引用
        ...:         prefix = cls.__name__
        ...:         index = cls.__counter
        ...:         self.storage_name = '_{}#{}'.format(prefix,index)  # 每个描述符名称都是唯一的
        ...:         cls.__counter += 1
        ...:     def __get__(self,instance,owner):  # 托管属性名称与实际存储的属性名称不同
        ...:         return getattr(instance,self.storage_name)
        ...:     def __set__(self,instance,value):
        ...:         if value>0:
        ...:             setattr(instance,self.storage_name, value)
        ...:         else:
        ...:             raise ValueError('value must be > 0')
    
    
    In [26]: class L:
        ...:     weight = QQ()  # 不需要再重复写 weight 了
        ...:     price = QQ()
        ...:     def __init__(self,des , weight, price):
        ...:         self.des = des
        ...:         self.weight = weight
        ...:         self.price = price
        ...:     def subtotal(self):
        ...:         return self.weight*self.price
    

    这里可以使用内置的高阶函数 getattrsetattr 存取值,无需使用 instance.__dict__,因为托管属性和储存属性的名称不同,所以把储存属性传给 getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归。

     如果想使用 Python 矫正名称的约定方式(例如 _L__quantity0),要知道托管类(即 L)的名称,可是,解释器要先运行类的定义体才能构建类,因此创建描述符实例时得不到那个信息。不过,对这个示例来说,为了防止不小心被子类覆盖,不用包含托管类的名称,因为每次实例化新的描述符,描述符类的 __counter 属性都会递增,从而确保每个托管类的每个储存属性的名称都是独一无二的。

    __get__ 方法的三个参数:self,instance,owner。owner 参数托管类 L 的引用,通过描述符从托管类中获取属性时用得到。

    如果使用类 L.weight 获取托管属性,描述符的 __get__ 方法 instance 参数是 None。会抛出异常。

    为了给用户提供内省和其他元编程技术支持,通过类访问托管属性时,返回描述符实例。

    class Quantity:
        __counter = 0
        def __init__(self):
            cls = self.__class__
            prefix = cls.__name__
            index = cls.__counter
            self.storage_name = '_{}#{}'.format(prefix, index)
            cls.__counter += 1
        def __get__(self, instance, owner):
            if instance is None:
                return self  # 返回描述符自身
            else:
                return getattr(instance, self.storage_name)
        def __set__(self, instance, value):
            if value > 0:
                setattr(instance, self.storage_name, value)
            else:
                raise ValueError('value must be > 0')
    

    将特性工厂函数实现不用属性名称

    def quantity():  # 没有 storage_name 参数
        try:
            quantity.counter += 1  # 一切皆对象,定义为 函数对象的属性,用于共享并不断累加
        except AttributeError:  # 捕捉没有属性异常
            quantity.counter = 0
    	
        # 生成的属性的闭包作用域的变量
        storage_name = '_{}:{}'.format('quantity', quantity.counter)
    
        def qty_getter(instance): ➎
            return getattr(instance, storage_name)
        def qty_setter(instance, value):
            if value > 0:
                setattr(instance, storage_name, value)
            else:
                raise ValueError('value must be > 0')
    
        return property(qty_getter, qty_setter)
    

    描述符类与工厂函数

    1 工厂函数只能粘贴,而描述符类可以继承

    2 工厂函数的闭包对比,描述符类更容易理解

    一种新型描述符

    问题:如果商品描述信息为空,导致无法下单。

    解决:创建一个 NonBlank 描述符。校验描述是否为空。

    一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体的行为。

    私有类属性

    In [3]: class A:
       ...:     __counter = 0  # 私有类属性
       ...:     def __init__(self):
       ...:         cls = self.__class__
       ...:         prefix = cls.__name__
       ...:         index = cls.__counter
       ...:         self.storage_name = '_{}#{}'.format(prefix, index)
       ...:
    
    In [4]: a = A()
    
    In [5]: a.__counter
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-5-8bd0ca4838da> in <module>
    ----> 1 a.__counter
    
    AttributeError: 'A' object has no attribute '__counter'
    
    In [6]: A.__counter
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-6-95bc9c924f7c> in <module>
    ----> 1 A.__counter
    
    AttributeError: type object 'A' has no attribute '__counter'
    
    In [7]: A.__dict__
    Out[7]:
    mappingproxy({'__module__': '__main__',
                  '_A__counter': 0,
                  '__init__': <function __main__.A.__init__(self)>,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    

    实现 Q 和 N 描述符

    In [8]: class A:  # 提供描述符的基础功能
       ...:     __counter = 0
       ...:     def __init__(self):
       ...:         cls = self.__class__
       ...:         prefix = cls.__name__
       ...:         index = cls.__counter
       ...:         self.storage_name = '_{}#{}'.format(prefix, index)
       ...:         cls.__counter += 1
       ...:     def __get__(self,instance, owner):
       ...:         if instance is None:
       ...:             return self
       ...:         else:
       ...:             return getattr(instance,self.storage_name)
       ...:     def __set__(self,instance,value):
       ...:         setattr(instance,self.storage_name,value)
       ...:
    
    In [9]: class V(abc.ABC, A):  # 继承自基础描述符类
       ...:     def __set__(self,instance,value):  # 重写 __set__ 方法
       ...:         value = self.validate(instance,value)  # 增加验证设置值的有效性
       ...:         super().__set__(instance,value)
       ...:     @abc.abstractmethod
       ...:     def validate(self,instance,value):  # 抽象方法,接口
       ...:         """return validated value or raise ValueError"""
       ...:
    
    In [10]: class Q(V):
        ...:     def validate(self,instance,value):
        ...:         if value <= 0:
        ...:             raise ValueError('value must be > 0')
        ...:         return value
        ...:
    
    In [12]: class N(V):
        ...:     def validate(self,instance,value):
        ...:         value = value.strip()
        ...:         if not value:
        ...:             raise ValueError('value cannot be empty or blank')
        ...:         return value
    

    使用 Q 和 N 描述符

    In [23]: class L:
        ...:     des = N()
        ...:     w = Q()
        ...:     p = Q()
        ...:     def __init__(self,des,w,p):
        ...:         self.des = des
        ...:         self.w = w
        ...:         self.p = p
        ...:     def subtotal(self):
        ...:         return self.w * self.p
    

    描述符的典型用途:管理数据属性。这种描述符也叫覆盖型描述符。

    覆盖型与非覆盖型描述符对比

    如前所述,Python 存取属性的方式特别不对等。通过实例读取属性时,通常返回的是实例中定义的属性;但是,如果实例中没有指定的属性,那么会获取类属性。而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类。

    In [30]: class O:  # 有 __get__ 和 __set__ 方法的典型 覆盖型描述符
        ...:     def __get__(self,instance,owner):
        ...:         print('get',self, instance,owner)
        ...:     def __set__(self,instance,value):
        ...:         print('set',self,instance,value)
        ...:
    
    In [31]: class O1:  # 没有 __set__ 方法的非覆盖型描述符,非数据描述符,遮盖型描述符
        ...:     def __get__(self,instance,owner):
        ...:         print('get',self,instance,owner)
        ...:
    
    In [32]: class O2:  # 没有 __get__ 方法的覆盖型描述符
        ...:     def __set__(self,instance,value):
        ...:         print('set',self,instance,value)
        ...:
    
    In [33]: class M:  # 托管类
        ...:     o = O()
        ...:     o1 = O1()
        ...:     o2 = O2()
        ...:     def spam(self):
        ...:         print(self)
    

    覆盖型描述符:实现 __set__ 方法的描述符。描述符是类属性,会覆盖对实例属性的赋值操作。

    特性 是覆盖型描述符,如果没有设值函数,property 的 __set__ 方法会抛出 AttributeError 异常,指明属性为只读。

    覆盖型描述符

    In [43]: m = M()
    
    In [44]: m.o
    get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
    
    In [45]: M.o  # 类触发,instance 为 None
    get <__main__.O object at 0x000001E40ACFE2E8> None <class '__main__.M'>
    
    In [46]: m.o = 1  # 赋值
    set <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> 1
    
    In [47]: m.o  # 读取仍会触发描述符 __get__ 方法
    get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
    
    In [49]: vars(m)
    Out[49]: {}
    
    In [50]: m.__dict__['aaa'] = 1  # 跳过描述符
    
    In [51]: m.aaa
    Out[51]: 1
        
    In [52]: vars(m)
    Out[52]: {'aaa': 1}
        
    In [55]: m.__dict__['o'] = 3  # 尝试赋值 o
    
    In [56]: vars(m)
    Out[56]: {'aaa': 1, 'o': 3}
    
    In [57]: m.o  # 描述符优先级高于对象属性
    get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
    

    没有 __get__ 方法的覆盖型描述符

    只实现了 __set__ 方法,也是覆盖型描述符,因此,只有写操作由描述符处理。通过实例读取描述符会返回描述符对象本身,因为没有处理读操作的 __get__ 方法。如果直接通过实例的 __dict__ 属性创建同名实例属性,以后,设置,仍会经过 __set__ 方法,读取,直接从实例中返回新赋予的值,而不会返回描述符对象。

    In [1]: class O:
       ...:     def __get__(self,instance,owner):
       ...:         print('get',self,instance,owner)
       ...:     def __set__(self,instance,value):
       ...:         print('set',self,instance,value)
       ...:
    
    In [2]: class O_get:
       ...:     def __get__(self,instance,owner):
       ...:         print('get',self,instance,owner)
       ...:
    
    In [3]: class O_set:
       ...:     def __set__(self,instance,value):
       ...:         print('set',self,instance,value)
       ...:
    
    In [4]: class T:
       ...:     o = O()
       ...:     o_get = O_get()
       ...:     o_set = O_set()
       ...:     def p(self):
       ...:         print(self)
       ...:
    
    In [5]: t = T()
    
    In [6]: t.o_set  # 这个覆盖型描述符没有 __get__ 方法,因此从类中获取描述符实例
    Out[6]: <__main__.O_set at 0x16dff9d0828>
    
    In [7]: T.o_set  # 直接读取描述符实例
    Out[7]: <__main__.O_set at 0x16dff9d0828>
    
    In [8]: t.o_set = 111  # 触发 __set__ 方法
    set <__main__.O_set object at 0x0000016DFF9D0828> <__main__.T object at 0x0000016DFFA093C8> 111
    
    In [9]: t.o_set
    Out[9]: <__main__.O_set at 0x16dff9d0828>
    
    In [10]: t.__dict__['o_set'] = 111  # 跳过 __set__ 方法
    
    In [11]: t.o_set  # 实例属性覆盖了描述符,读操作
    Out[11]: 111
    
    In [12]: t.o_set = 2  # 仍然触发 __set__
    set <__main__.O_set object at 0x0000016DFF9D0828> <__main__.T object at 0x0000016DFFA093C8> 2
    
    In [13]: t.o_set
    Out[13]: 111
    

    非覆盖型描述符

    没有实现 __set__ 方法的描述符是 非覆盖型描述符。如果设置了同名的实例属性,描述符会被覆盖,致使描述符无法处理那个实例的那个属性。

    In [14]: t.o_get  # 触发 __get__ 方法
    get <__main__.O_get object at 0x0000016DFF9D0EF0> <__main__.T object at 0x0000016DFFA093C8> <class '__main__.T'>
    
    In [15]: t.o_get = 111  # 没有 __set__ 方法,成功赋值对象属性
    
    In [16]: t.o_get  # 成功 覆盖了 __get__ 方法
    Out[16]: 111
    
    In [17]: T.o_get
    get <__main__.O_get object at 0x0000016DFF9D0EF0> None <class '__main__.T'>
    
    In [18]: del t.o_get  # 删除实例属性
    
    In [19]: t.o_get
    get <__main__.O_get object at 0x0000016DFF9D0EF0> <__main__.T object at 0x0000016DFFA093C8> <class '__main__.T'>
    

    覆盖型描述符也叫数据描述符或强制描述符。非覆盖型描述符也叫非数据描述符或遮盖型描述符。

    在类中覆盖描述符

    不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。

    In [21]: T.o = 2
    
    In [22]: t.o
    Out[22]: 2
        
    In [23]: del T.o
    
    In [24]: t.o
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-24-a0deb82dff1e> in <module>
    ----> 1 t.o
    
    AttributeError: 'T' object has no attribute 'o'
        
    示例揭示了读写属性的另一种不对等:读类属性的操作可以由依附在托管类上定义
    有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有
    __set__ 方法的描述符处理。
    
    若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。
    默认情况下,对用户定义的类来说,其元类是 type,而我们不能为 type 添加属
    性。
    

    方法是描述符

    在类中定义的函数属于绑定方法(bound method),因为用户定义的函数都有 __get__ 方法,所以依附到类上时,就相当于描述符。

    In [25]: t.p
    Out[25]: <bound method T.p of <__main__.T object at 0x0000016DFFA093C8>>
    
    In [26]: T.p
    Out[26]: <function __main__.T.p(self)>
    
    In [27]: t.p = 3  # 函数没有实现 __set__ 方法,因此是非覆盖型描述符
    
    In [28]: t.p
    Out[28]: 3
    

    t.p 返回的是绑定方法对象:一种可调用的对象,包装了函数,并把托管实例绑定给函数的第一个参数(self),这与 functools.partial 函数的行为一致。

    T.p 返回函数的 __get__ 方法返回自身的引用。

    In [29]: import collections
    
    In [31]: class Text(collections.UserString):
        ...:     def __repr__(self):
        ...:         return 'Text({!r})'.format(self.data)
        ...:     def reverse(self):
        ...:         return self[::-1]
        ...:
    
    In [32]: w = Text('abc')
    
    In [33]: w
    Out[33]: Text('abc')
    
    In [34]: w.reverse()
    Out[34]: Text('cba')
    
    In [35]: type(Text.reverse)  # 类调用:函数
    Out[35]: function
    
    In [36]: type(w.reverse)  # 对象调用:方法
    Out[36]: method
    
    In [37]: Text.reverse(w)
    Out[37]: Text('cba')
    
    In [38]: list(map(Text.reverse,['fdg',(1,2,3),Text('kjl')]))  # 函数可以处理实例之外的对象
    Out[38]: ['gdf', (3, 2, 1), Text('ljk')]
    
    In [39]: Text.reverse.__get__(None,Text)  # 得到函数本身
    Out[39]: <function __main__.Text.reverse(self)>
    
    In [40]: Text.reverse.__get__(w)  # 得到绑定方法
    Out[40]: <bound method Text.reverse of Text('abc')>
    
    In [41]: w.reverse  # 实际调用了 w.reverse.__get__(w)
    Out[41]: <bound method Text.reverse of Text('abc')>
    
    In [42]: w.reverse.__self__  # 绑定方法对象调用实例对象引用
    Out[42]: Text('abc')
    
    In [44]: w.reverse.__func__  # 原始函数引用
    Out[44]: <function __main__.Text.reverse(self)>
    
    In [45]: Text.reverse
    Out[45]: <function __main__.Text.reverse(self)>
        
    In [46]: w.reverse.__set__  # 没有 __set__ 的描述符
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-46-571c9c95520e> in <module>
    ----> 1 w.reverse.__set__
    
    AttributeError: 'function' object has no attribute '__set__'
    
    In [47]: w.reverse.__get__  # 非覆盖型描述符
    Out[47]: <method-wrapper '__get__' of method object at 0x0000016DFF92EEC8>
    

    绑定方法对象有 __call__ 方法,用于被调用是触发。这个方法会调用 __func__ 属性引用的原始函数,并把第一个参数设置为__self__ 属性。这就是形参 self 的隐式绑定方式。

    函数会变成绑定方法,这是 Python 语言底层使用描述符的最好例证。

    描述符用法建议

    使用特性以保持简单

    只读描述符必须有 __set__ 方法:否则,会被同名属性覆盖。

    用于验证的描述符可以只定义 __set__ 方法

    仅有 __get__ 方法的描述符可以实现高速缓存:非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。之后,都不会触发 __get__ 方法了。

    非特殊方法可以被实例属性覆盖:由于函数和方法只实现了 __get__ 方法,会被覆盖。然而,特殊方法不受影响。解释器只会在类中寻找特殊方法,例如:repr(x) 执行的是 x.__class__.__repr__(x) ,因此对象中的同名属性不会覆盖特殊方法。

    特殊方法,类方法,静态方法,特性 不能被实例属性覆盖。

    描述符文档字符串和覆盖删除操作

    描述符类的文档字符串用于注解托管类中的各个描述符实例。帮助界面:help(L.weight)

    描述符:__get__ __set__ __delete__

  • 相关阅读:
    Codeforces F. Bits And Pieces(位运算)
    一场comet常规赛的台前幕后
    【NOIP2019模拟2019.9.4】B(期望的线性性)
    「NOI2016」循环之美(小性质+min_25筛)
    【NOI2011】兔农(循环节)
    LOJ #6538. 烷基计数 加强版 加强版(生成函数,burnside引理,多项式牛顿迭代)
    noi2019感想
    7.12模拟T2(套路容斥+多项式求逆)
    CF 848E(动态规划+分治NTT)
    CF 398 E(动态规划)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15793683.html
Copyright © 2020-2023  润新知