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

探寻有趣之事!
目录
相关文章
|
7月前
|
机器学习/深度学习 算法 数据挖掘
统计学与Python:实现描述性统计和推断性统计分析
【4月更文挑战第12天】本文介绍了Python在统计学中的应用,重点讲解了如何使用Python进行描述性与推断性统计分析。Pandas和NumPy库用于描述性统计,提供数据概括和总结功能;Scipy和Statsmodels库支持推断性统计,包括假设检验和模型建立。通过数据导入、描述性统计量计算、图表绘制以及假设检验和回归分析等步骤,展示了Python实现统计分析的基本流程。持续学习和实践将有助于提升Python统计分析能力。
496 0
|
7月前
|
缓存 API 数据库
Python黑魔法解密:深入探究元编程
【2月更文挑战第9天】在Python世界中,元编程是一种强大而神秘的技术。通过元编程,我们可以在运行时动态地创建、修改和操作代码,为程序增加灵活性和扩展性。本文将带您深入探究Python中的元编程,揭示其中的黑魔法,并展示其在实际应用中的威力。
53 3
|
数据挖掘 计算机视觉 Python
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
96 1
Python数据分析中图像处理的实用技术点:图像加载与保存、图像转换与增强、特征提取与描述
|
7月前
|
Python
请描述 Python 中的模块和包的概念,以及如何导入和使用它们。
请描述 Python 中的模块和包的概念,以及如何导入和使用它们。
50 3
|
7月前
|
索引 Python 容器
Python黑魔法揭秘:超强公共操作符和推导式的编程技巧-1
Python黑魔法揭秘:超强公共操作符和推导式的编程技巧-1
|
5月前
|
Python
Python黑魔法揭秘:闭包与装饰器的高级玩法,让你代码飞起来
【7月更文挑战第7天】Python的闭包和装饰器是提升代码效率的神器。闭包是能记住外部作用域变量的内部函数,常用于动态函数创建。示例中,`make_multiplier_of`返回一个保留`n`值的闭包。装饰器则是一个接收函数并返回新函数的函数,用于在不修改原函数情况下添加功能,如日志或性能追踪。`@my_decorator`装饰的`say_hello`函数在执行时会自动加上额外操作。掌握这两者,能让Python代码更优雅、强大。**
36 1
|
5月前
|
开发者 Python
【Python】已解决:TypeError: descriptor ‘index‘ for ‘list‘ objects doesn‘t apply to a ‘str‘ object
【Python】已解决:TypeError: descriptor ‘index‘ for ‘list‘ objects doesn‘t apply to a ‘str‘ object
151 0
|
7月前
|
Java C# 开发者
Python 中的类型注解是一种用于描述变量、函数参数和返回值预期类型的机制
【5月更文挑战第8天】Python的类型注解提升代码可读性和可维护性,虽非强制,但利于静态类型检查(如Mypy)。包括:变量注解、函数参数和返回值注解,使用内置或`typing`模块的复杂类型,自定义类型注解,以及泛型模拟。类型注解可在变量声明、函数定义和注释中使用,帮助避免类型错误,提高开发效率。
76 6
|
7月前
|
前端开发 JavaScript 数据管理
描述一个使用Python开发Web应用程序的实际项目经验,包括所使用的框架和技术栈。
使用Flask开发Web应用,结合SQLite、Flask-SQLAlchemy进行数据管理,HTML/CSS/JS(Bootstrap和jQuery)构建前端。通过Flask路由处理用户请求,模块化代码提高可维护性。unittest进行测试,开发阶段用内置服务器,生产环境可选WSGI服务器或容器化部署。实现了用户注册登录和数据管理功能,展示Python Web开发的灵活性和效率。
55 4
|
7月前
|
数据采集 数据可视化 数据挖掘
python数据分析——业务数据描述
业务数据描述将从统计学角度来分析这指标。利用统计方法,数据分析人员可以通过相应统计模型开展数据分析。数据分析过程包括数据收集,数据处理,数据探索,模型方法应用,分析结果数据展现及形成分析报告。 业务报表是指对业务内容和数据的统计分析图表。统计图表代表了一张图像化的数据,形象地呈现数据。我们常常提到的可视化分析图表一般包括比较类图表,占比类图表,相关类图表和趋势类图表。
122 1
下一篇
DataWorks