描述符
- 描述符简介
- 描述符协议
- 调用描述符
- 描述符示例
- 常见的描述符
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
、绑定方法、staticmethod
和classmethod
都基于描述符协议。
2 描述符协议
描述符协议包含三个方法:
descr.__get__(self, instance, owner)
descr.__set__(self, instance, value)
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
是实例对象或类而有所不同:
- 对于实例objects(对象):
object.getattribute()
将b.x
转换为b.__dict__['x'].__get__(b, type(b))
- 对于classes(类):
type.getattribute()
将B.x
转换为B.__dict__['x'].__get__(None, B)
4 描述符示例
我们先定义一些术语:
- 描述符类:实现描述符协议的类(示例1中的
Number
类)。 - 托管类:将描述符实例声明为类属性的类(示例1中的
Goods
类)。 - 描述符实例:描述符类的各个实例,声明为托管类的类属性。
- 托管实例:托管类的实例对象。
- 存储属性:托管实例中存储自身托管属性的属性。
- 托管属性:托管类中由描述符实例处理的公开属性,值存储在存储属性中。
存储属性和托管属性可能看定义难以区分,在这里举例说明:示例1中,存储属性与托管属性的名称相同,即price
、left_num
;示例2中,存储属性为_Number#0
、_Number#1
,托管属性为price
、left_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.price
或Goods.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