描述符协议

描述符描述符简介描述符协议调用描述符描述符示例常见的描述符1 简介Python 描述符是对多个属性运用同种存取逻辑的一种方式。如,Django ORM和SQL Alchemy中的字段类型就是描述符,把数据库记录中的字段里的数据与Python对象的属性对应起来。Python 没有私有变量的概念,而描述符协议可以作为一种 Python 的方式来实现与私有变量类似的功能。一般来说,...
摘要由CSDN通过智能技术生成

描述符

  • 描述符简介
  • 描述符协议
  • 调用描述符
  • 描述符示例
  • 常见的描述符

1 简介

Python 描述符是对多个属性运用同种存取逻辑的一种方式。如,Django ORM和SQL Alchemy中的字段类型就是描述符,把数据库记录中的字段里的数据与Python对象的属性对应起来。

Python 没有私有变量的概念,而描述符协议可以作为一种 Python 的方式来实现与私有变量类似的功能。

一般来说,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。这些方法是__get__()__set__()__delete__()。如果对象定义了任意这些方法,则称其为描述符。

属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x有一个查找链,从a.__dict__['x'],然后type(a).__dict__['x'],继续通过type(a)排除元类的基类。如果查找的值是定义其中一个描述符方法的对象,则Python可以覆盖默认行为并调用描述符方法。

描述符是Python的独有特性,也很常见,property、绑定方法、staticmethodclassmethod都基于描述符协议。

2 描述符协议

描述符协议包含三个方法:

  1. descr.__get__(self, instance, owner)
  2. descr.__set__(self, instance, value)
  3. descr.__delete__(self, instance)
    • __get__用于获取托管类的属性或托管实例的属性。
    • 参数owner代表的是托管类,即将描述符类声明为类属性的类。
    • instance代表的是托管实例,即托管类的实例对象。

定义任何这些方法的对象,都将被视为描述符,并且在查找属性时可以覆盖默认行为。

如果一个对象同时定义__get__()__set__(),它被认为是一个数据描述符。仅定义__get__()的描述符称为非数据描述符

数据和非数据描述符的不同之处在于如何针对实例字典中的条目计算覆盖。如果实例的字典具有与数据描述符同名的条目,则数据描述符优先。如果实例的字典具有与非数据描述符同名的条目,则字典条目优先。

如果要实现只读数据描述符,必须定义__get__()__set__(),并且__set__()被调用时应该抛出AttributeError异常。使用异常占位定义__set__()方法足以使其成为数据描述符。

3 调用描述符

描述符可以通过其方法名称直接调用,如 d.__get__(obj);或者,更常见的是在属性访问时自动调用描述符。例如,obj.d,在obj的字典中查找d。如果d定义了__get__(),则根据下面列出的优先规则调用。优先级链使数据描述符优先于实例变量,实例变量优先于非数据描述符。当然,调用的细节根据obj是实例对象或类而有所不同:

  1. 对于实例objects(对象):object.getattribute()b.x转换为b.__dict__['x'].__get__(b, type(b))
  2. 对于classes(类):type.getattribute()B.x转换为B.__dict__['x'].__get__(None, B)

4 描述符示例

我们先定义一些术语:

  • 描述符类:实现描述符协议的类(示例1中的Number类)。
  • 托管类:将描述符实例声明为类属性的类(示例1中的Goods类)。
  • 描述符实例:描述符类的各个实例,声明为托管类的类属性
  • 托管实例:托管类的实例对象。
  • 存储属性:托管实例中存储自身托管属性的属性。
  • 托管属性:托管类中由描述符实例处理的公开属性,值存储在存储属性中。

​ 存储属性和托管属性可能看定义难以区分,在这里举例说明:示例1中,存储属性与托管属性的名称相同,即priceleft_num;示例2中,存储属性为_Number#0_Number#1,托管属性为priceleft_num。总的来说,存储属性的名称存储在实例对象__dict__中,而实例对象调用属性引起描述符实例调度(会调用描述符方法)的即托管属性。

示例1:验证属性

我们创建了一个叫Number的描述符类,用于托管跟数字有关的属性;描述符类有个name的属性,这是托管实例的属性名称。

#example1.py

class Number:
    """描述符类,用于托管数量信息
    	name:商品的属性名称:价格、剩余量等
    """
    def __init__(self, name):
        self.name = name
    
    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.name] = value
        else:
            raise ValueError("'%s' must be > 0"%self.name)

class Goods:
    price = Number('price')
    left_num = Number('left_num')
    
    def __init__(self, goods_name, price, left_num):
        self.goods_name = goods_name
        self.price = price
        self.left_num = left_num
>>> rabbit = Goods('大白兔', 0.2, 500)
>>> rabbit.price
0.2
>>> rabbit.left_num
500
>>> rabbit.__dict__
{
   'goods_name': '大白兔', 'price': 0.2, 'left_num': 500}
>>> spicy = Goods('辣条', -0.5, 200)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __init__
  File "<stdin>", line 12, in __set__
ValueError: 'price' must be > 0
>>> 

描述符类定义了__set__方法,尝试为托管属性赋值时,会调用__set__方法,参数self是描述符实例,即Goods.priceGoods.left_num。如预期的,不能设置小于0的数。

__set__使用instance.__dict__[self.name]=value设置托管实例的属性值;因为托管属性的名称和存储属性一致,读值方法也不需要特殊逻辑,所以Number类不需要定义__get__方法。

示例1代码有个缺点,就是实例化描述符时需要重复输入属性的名称,我们希望可以不需要输入名称,如下,我们将在示例2中实现。

class Goods:
	price = Number()
	left_num = Number()
	...#余下方法相同

示例2:自动获取存储属性的名称

#example2.py

class Number:
    __counter = 0
    
    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        suffix = cls.__counter
        self.name = '_%s#%d'%(prefix,suffix)
        cls
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值