司开星的博客

Python 描述器

定义

一个类中如果定义了__get__()__set__()__delete__() 三种方法则被称为描述器。仅定义了__get__()方法的称为非资料描述器, 定义了__get__()__set__() 方法的称为资料描述器。

描述器的主要用处是拦截某个类中的属性调用。

用处

从@property说起

要在类中定义一个调用时动态变化的属性可以使用 @property 装饰器。比如定义一个鸡蛋类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

class Egg(object):
def __init__(self):
self.price = 10 # 人民币价格

@property
def usd_price(self): # 美元价格
return self.price / 6.8

@usd_price.setter
def usd_price(self, value):
self.price = value * 6.8

@property
def hkd_price(self): # 港元价格
return self.price / 0.87

@hkd_price.setter
def hkd_price(self, value):
self.price = value * 0.87

egg = Egg()

print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))
egg.usd_price = 2
print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))
egg.hkd_price = 15
print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))

输出:

1
2
3
4

当前鸡蛋价格:10 人民币,1.4705882352941178 美元,11.494252873563218 港元
当前鸡蛋价格:13.6 人民币,2.0 美元,15.632183908045977 港元
当前鸡蛋价格:13.05 人民币,1.9191176470588236 美元,15.000000000000002 港元

可以发现美元和港元价格的代码逻辑基本相同,如果需要更多货币价格则会出现更多重复代码。

用描述器实现

这时候就轮到描述器上场了。首先定义一个实现了描述器协议的价格类:

1
2
3
4
5
6
7
8
9
class Price(object):
def __init__(self, rate):
self.rate = rate

def __get__(self, instance, instype):
return instance.price / self.rate

def __set__(self, instance, value):
instance.price = value * self.rate

接着用描述器重构上面的Egg类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Egg(object):
def __init__(self):
self.price = 10 # 人民币价格

usd_price = Price(6.8)
hkd_price = Price(0.87)

egg = Egg()

print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))
egg.usd_price = 2
print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))
egg.hkd_price = 15
print("当前鸡蛋价格:%s 人民币,%s 美元,%s 港元" % (egg.price, egg.usd_price, egg.hkd_price))

实例化描述器类即可,具体的代码逻辑都在描述器里定义好了。这里可以简单地将描述器理解成把 @property 的三个被装饰的方法抽象出来作为描述器类的类方法,以方便复用。

用描述器实现的Python语法

@property

@property本身也是通过描述器实现的。其 Python 等价代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)

def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)

def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)

class Egg(object):
def __init__(self):
self.price = 10 # 人民币价格

# 实例初始化时接收到一个参数 fget=self.usd_price() (原usd_price方法)
# 装饰后 usd_price 指向一个 Property 类的实例
@property
def usd_price(self):
return self.price / 6.8

# 由于 usd_price 现在是一个 Property 实例, 故存在 setter 方法
# 此装饰器新建一个 Property 实例,
# 并将原实例的 fget 和这个新定义的 usd_price 方法传递给新实例初始化
# 此时新实例已经有 self.fget 和 self.fset 方法了
# 并且名称仍然是 usd_price
@usd_price.setter
def usd_price(self, value):
self.price = value * 6.8

getattribute/get/getattr的区别

__getattribute__是新式类的实例取属性时直接调用的方法。这个方法也定义了之后的调用顺序。

__get__是上文说的描述器协议。

__getattr__是其他地方都找不到这个属性时最后调用的方法。

具体调用关系是:首先调用__getattribute____getattribute__中定义了之后的调用顺序。默认的__getattribute__中定义的查找顺序是:实例字典 –> 资料描述器 –> 类字典 –> 非资料描述器 –> __getattr__