• Python 黑魔法 --- 描述器(descriptor)


    import struct
    
    class StructField:
        '''
        使用了一个描述器(Descriptor representing )来表示每个结构字段,
            每个描述器包含一个结构兼容格式的代码以及一个字节偏移量, 存储在内部的内存缓冲中。
        '''
        def __init__(self, fmt, offset):
            self.format = fmt
            self.offset = offset
            
        def __get__(self, instance, cls):
            if instance is None:
                return self
            else:
                r = struct.unpack_from(self.format, instance._buffer, self.offset)
                return r[0] if len(r) == 1 else r
    
    class Structure:
        def __init__(self, bytedata):
            '''
            接受字节数据并存储在内部的内存缓冲中,并被 StructField 描述器使用。
            '''
            self._buffer = memoryview(bytedata)
    

    描述器是什么?

    只要具有 __get__ 方法的类就是描述符类。

    • 如果一个类中具有 __get____set__ 两个方法,那么就是数据描述符。
    • 如果一个类中只有 __get__ 方法,那么是非数据描述符。

    具体如下:

    • __get__:当我们用类或者实例来调用该属性时,Python 会返回 __get__ 函数的结果。
    • __set__:当我们用实例来设置属性值时,Python 会调用该函数。对类没有限制作用。
    • __delete__:当我们用实例试图删除该属性时,Python 会调用该函数。对类没有限制作用。

    非数据描述类

    class Desc:
        def __init__(self, value=22):
            self.value = value
            
        def __get__(self, ins, cls):
            return self.value
    
        
    class A:
        v = Desc()
        
    a = A()
    

    由于实例中没有 v 属性,所以找到了类的属性,而类的属性是一个描述符类实例,所以调用其 __get__ 方法的结果。

    a.v 
    
    22
    

    实例的 __dict__ 空空如也。

    a.__dict__      
    
    {}
    

    类的 __dict__ 中确实存在 v 属性,且是一个 Desc object 对象。

    A.__dict__      
    
    mappingproxy({'__module__': '__main__',
                  'v': <__main__.Desc at 0x248e0e79748>,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    

    我们通过实例设置 v 属性,则发现实例的 __dict__ 中存入了我们刚才设置的属性:

    a.v = 30     
    a.__dict__     
    
    {'v': 30}
    

    而类的 __dict__ 没有发生任何变化:

     A.__dict__     
    
    mappingproxy({'__module__': '__main__',
                  'v': <__main__.Desc at 0x248e0e79748>,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    
    a.v             # 
    
    30
    

    如我们所料,访问到了 a.__dict__ 中的内容。我们删除实例的属性 v 后发现居然还是可以调用 a.v,返回的是我们设置之前的值。

    del a.v        
    
    a.v
    
    22
    
    A.__dict__      # 和前面一样,没有发生变化。
    
    mappingproxy({'__module__': '__main__',
                  'v': <__main__.Desc at 0x248e0e79748>,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    

    通过上面的测试,我们发现非数据描述类有如下特点:

    • 如果实例 __dict__ 没有设置同名属性,那么返回描述类的 __get__ 方法的结果。
    • 如果实例 __dict__ 中存在同名属性,那么返回实例 __dict__ 中的内容。
    • 对我们设置实例的 __dict__ 中的行为并不做阻止。所以我说这是查看级别的描述类。

    数据描述类

    class Desc:
        def __init__(self, value=22):
            self.value = value
            
        def __get__(self, ins, cls):
            return self.value
        
        def __set__(self, ins, value):
            self.value = value
            #raise AttributeError
    
    class A:
        v = Desc()
        
    a = A()
    
    a.v
    
    22
    

    我们设置 a.v 后,发现实例的 __dict__ 中仍然空空如也。因为此时调用的是 __set__ 方法,值 10 存入到了 Desc 实例的 value 属性上了。

    a.v = 10
    a.__dict__     
    
    {}
    
    A.__dict__
    
    mappingproxy({'__module__': '__main__',
                  'v': <__main__.Desc at 0x248e0e25cf8>,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    
     a.v             # 此时得到的还是 Desc 的 __get__ 方法返回的结果。
    
    10
    
    del a.v         #不允许我们删除
    
    ---------------------------------------------------------------------------
    
    AttributeError                            Traceback (most recent call last)
    
    <ipython-input-22-ca186fdd8490> in <module>()
    ----> 1 del a.v         #不允许我们删除
    
    
    AttributeError: __delete__
    
    A.v = 30
    
    A.__dict__
    
    mappingproxy({'__module__': '__main__',
                  'v': 30,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    

    我们把 __set__ 方法的原来语句注销,添加 raise AttribeError 语句,再次运行:

    class Desc:
        def __init__(self, value=22):
            self.value = value
            
        def __get__(self, ins, cls):
            return self.value
        
        def __set__(self, ins, value):
            #self.value = value
            raise AttributeError
            
            
    class A:
        v = Desc()
        
    a = A()
    a.v = 30        
    
    ---------------------------------------------------------------------------
    
    AttributeError                            Traceback (most recent call last)
    
    <ipython-input-26-af36724697b5> in <module>()
         15 
         16 a = A()
    ---> 17 a.v = 30
    
    
    <ipython-input-26-af36724697b5> in __set__(self, ins, value)
          8     def __set__(self, ins, value):
          9         #self.value = value
    ---> 10         raise AttributeError
         11 
         12 
    
    
    AttributeError: 
    

    我们在 __set__ 中手动添加了 AttributeError 异常,所以我们再也不能设置 a.v 的值了,因此该属性鞭策了只读属性。

    A.v = 20          # 通过类,仍然可以改变属性
    
    A.__dict__      #改变后,变成了普通属性 20 了,这时甚至都已经不再是描述符类了。
    
    mappingproxy({'__module__': '__main__',
                  'v': 20,
                  '__dict__': <attribute '__dict__' of 'A' objects>,
                  '__weakref__': <attribute '__weakref__' of 'A' objects>,
                  '__doc__': None})
    
    del A.v
    

    说明如下:

    • __set__ 方法存在后,实例设置同名属性时,完全需要看 __set__ 的脸色。
    • 如果描述类中 __set__ 方法存在但是 __delete__ 方法不存在,那么不能删除客户类中的属性。
    • 即使在 __set__ 方法中做了限制,这个限制只是对实例而言的,对类没有起到作用。

    把属性存在描述符类中

    class Desc:
        def __init__(self, value):
            self.value = value
            
        def __get__(self, ins, cls):
            return self.value
        
        def __set__(self, ins, value):
            self.value = value
            
        def __delete__(self, ins):
            raise AttributeError('not allowed to delete attribute name ' )
    
            
    class A:
        name = Desc('JS')
        
    a = A()
    
    del a.name
    
    ---------------------------------------------------------------------------
    
    AttributeError                            Traceback (most recent call last)
    
    <ipython-input-33-a495b342d7d6> in <module>()
    ----> 1 del a.name
    
    
    <ipython-input-32-27df162f0b50> in __delete__(self, ins)
         10 
         11     def __delete__(self, ins):
    ---> 12         raise AttributeError('not allowed to delete attribute name ' )
         13 
         14 
    
    
    AttributeError: not allowed to delete attribute name 
    
    a = A()
    b = A()
    
    a.name
    
    'JS'
    
    b.name
    
    'JS'
    
    a.name = 'CC'
    
    b.name
    
    'CC'
    

    缺点显而易见,如果有多个实例,那么他们共享一个描述符,所以当一个实例的该属性发生改变后,其他实例的该属性也会发生变化。

    改善方法:

    存入一个字典,把实例的 hash 作为健存入,这样可以解决问题。

    class Desc:
        def __init__(self, value):
            self.values = {}
            
        def __get__(self, ins, cls):
            return self.values[hash(ins)] 
        
        def __set__(self, ins, value):
            self.values[hash(ins)] = value
            
        def __delete__(self, ins):
            raise AttributeError('not allowed to delete attribute name ' )
    

    把数据存入实例中

    class Desc:
        def __get__(self, ins, cls):
            return ins._name
        
        def __set__(self, ins, value):
            ins._name = value 
            
        def __delete__(self, ins):
            raise AttributeError('not allowed to delete attribute name ' )
    
            
    class A:
        name = Desc
        
    a = A()
    
    a.name = 'JS'
    
    a.name
    
    'JS'
    
    a._name = 'CC'
    
    a.name
    
    'JS'
    

    缺点:我们设置在实例中的变量私密性不太好,可以很容易被改变。

    当然,可以做一个私有性的装饰器,或者利用属性扩张来解决。

    补充解释

    • __get__(self, ins, cls):其中 ins 为实例对象,在我们上面的例子中是 a 或者 bclsa 或者 b 的类,为 A
    • __set____delete__ins 和上面的含义相同

    总结

    一个描述器就是一个实现了三个核心的属性访问操作(get, set, delete)的类, 分别为 __get__()__set__()__delete__() 这三个特殊的方法。 这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。

    # Descriptor attribute for an integer type-checked attribute
    class Integer:
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, cls):
            if instance is None:
                return self
            else:
                return instance.__dict__[self.name]
    
        def __set__(self, instance, value):
            if not isinstance(value, int):
                raise TypeError('Expected an int')
            instance.__dict__[self.name] = value
    
        def __delete__(self, instance):
            del instance.__dict__[self.name]
    

    为了使用一个描述器,需将这个描述器的实例作为类属性放到一个类的定义中。例如:

    class Point:
        x = Integer('x')
        y = Integer('y')
    
        def __init__(self, x, y):
            self.x = x
            self.y = y
    

    当你这样做后,所有对描述器属性(比如 xy)的访问会被 __get__()__set__()__delete__() 方法捕获到。例如:

    p = Point(2, 3)
    p
    
    <__main__.Point at 0x248e0ef4b38>
    
    p.x # Calls Point.x.__get__(p,Point)
    
    2
    
    p.y = 5 # Calls Point.y.__set__(p, 5)
    
    p.x = 2.3 # Calls Point.x.__set__(p, 2.3)
    
    ---------------------------------------------------------------------------
    
    TypeError                                 Traceback (most recent call last)
    
    <ipython-input-52-606a2a07c22c> in <module>()
    ----> 1 p.x = 2.3 # Calls Point.x.__set__(p, 2.3)
    
    
    <ipython-input-46-2df920d030da> in __set__(self, instance, value)
         12     def __set__(self, instance, value):
         13         if not isinstance(value, int):
    ---> 14             raise TypeError('Expected an int')
         15         instance.__dict__[self.name] = value
         16 
    
    
    TypeError: Expected an int
    

    作为输入,描述器的每一个方法会接受一个操作实例。 为了实现请求操作,会相应的操作实例底层的字典(__dict__属性)。 描述器的 self.name 属性存储了在实例字典中被实际使用到的key

  • 相关阅读:
    子类继承方法的重写
    操作系统的用户模式和内核模式
    Java中的CAS
    FaceBook SDK登录功能实现(Eclipse)
    eclipse集成ijkplayer项目
    android handler传递数据
    android发送短信
    hadoop中的job.setOutputKeyClass与job.setMapOutputKeyClass
    mysql对事务的支持
    使用jd-gui+javassist修改已编译好的class文件
  • 原文地址:https://www.cnblogs.com/q735613050/p/9248574.html
Copyright © 2020-2023  润新知