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

简介: import structclass StructField: ''' 使用了一个描述器(Descriptor representing )来表示每个结构字段, 每个描述器包含一个结构兼容格式的代码以及一个字节偏移量, 存储在内部的内存缓冲中。
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

探寻有趣之事!
目录
相关文章
|
2月前
|
缓存 API 数据库
Python黑魔法解密:深入探究元编程
【2月更文挑战第9天】在Python世界中,元编程是一种强大而神秘的技术。通过元编程,我们可以在运行时动态地创建、修改和操作代码,为程序增加灵活性和扩展性。本文将带您深入探究Python中的元编程,揭示其中的黑魔法,并展示其在实际应用中的威力。
19 3
|
1月前
|
Python
请描述 Python 中的模块和包的概念,以及如何导入和使用它们。
请描述 Python 中的模块和包的概念,以及如何导入和使用它们。
23 3
|
3月前
|
索引 Python 容器
Python黑魔法揭秘:超强公共操作符和推导式的编程技巧-1
Python黑魔法揭秘:超强公共操作符和推导式的编程技巧-1
|
4月前
|
数据挖掘 计算机视觉 Python
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
44 1
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
|
27天前
|
前端开发 JavaScript 数据管理
描述一个使用Python开发Web应用程序的实际项目经验,包括所使用的框架和技术栈。
使用Flask开发Web应用,结合SQLite、Flask-SQLAlchemy进行数据管理,HTML/CSS/JS(Bootstrap和jQuery)构建前端。通过Flask路由处理用户请求,模块化代码提高可维护性。unittest进行测试,开发阶段用内置服务器,生产环境可选WSGI服务器或容器化部署。实现了用户注册登录和数据管理功能,展示Python Web开发的灵活性和效率。
15 4
|
1月前
|
安全 测试技术 API
请描述在 Python WEB 开发中常用的测试方法。
请描述在 Python WEB 开发中常用的测试方法。
18 0
|
1月前
|
SQL 关系型数据库 数据库连接
描述一下在 Python 中使用数据库的常见方法。
【2月更文挑战第26天】【2月更文挑战第91篇】描述一下在 Python 中使用数据库的常见方法。
|
1月前
|
数据采集 数据可视化 数据挖掘
python数据分析——业务数据描述
业务数据描述将从统计学角度来分析这指标。利用统计方法,数据分析人员可以通过相应统计模型开展数据分析。数据分析过程包括数据收集,数据处理,数据探索,模型方法应用,分析结果数据展现及形成分析报告。 业务报表是指对业务内容和数据的统计分析图表。统计图表代表了一张图像化的数据,形象地呈现数据。我们常常提到的可视化分析图表一般包括比较类图表,占比类图表,相关类图表和趋势类图表。
40 1
|
2月前
|
缓存 Python
什么是Python中的描述符(Descriptor)?如何实现一个描述符?
什么是Python中的描述符(Descriptor)?如何实现一个描述符?
|
3月前
|
Python
请描述Python中类的私有属性和公有属性的不同,并给出使用私有属性的好处。
请描述Python中类的私有属性和公有属性的不同,并给出使用私有属性的好处。
47 1