描述器(2)-应用

这里主要是说明一下描述器的应用,python提供了一个property的描述器,常用于做装饰器。但是如果在一个应用场景中对于某类型的属性都需要做相同的处理的时候就需要写很多的property,这时候如果能自定义描述器就可以简便处理。下面就分别讲自定义的描述器和实际的应用以及property的应用和原理。

自定义描述器

从上面一节可以看到我们给出了一个用于分数描述器的示例,这里再展示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class score_desc(object):
def __set__(self, obj, score):
print 'call __set__'
if score >= 0:
self.score = score
else:
raise Exception("score<0!")

def __get__(self, obj, obj_type=None):
print 'call __get__'
return self.score


class stu_scores(object):
math_score = score_desc()
chinese_score = score_desc()

这里有一个严重的问题,就是stu_scores的math_score或者chinese_score都是类属性,这样导致如果有多个学生将无法记录。所以描述器还需要针对不同的实例给不同的存储,这时候一般使用字典,键值是实例。
修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# coding=utf8
from weakref import WeakKeyDictionary

class score_desc(object):
def __init__(self, default_score=0):
self.data = WeakKeyDictionary() #弱引用字典
self.default_score = default_score

def __set__(self, obj, score):
if score >= 0:
self.data[obj] = score
else:
raise Exception("score<0!")

def __get__(self, obj, obj_type=None):
return self.data.get(obj, self.default_score)

测试:

1
2
3
4
5
6
7
8
stu1 = stu_scores()
stu1.math_score = 90
stu2 = stu_scores()
stu2.math_score = 80
print stu1.math_score
print stu2.math_score
print stu1.chinese_score
stu1.chinese_score = -10

输出:

1
2
3
4
5
6
7
8
9
90
80
0
Traceback (most recent call last):
File "descript_test.py", line 58, in <module>
stu1.chinese_score = -10
File "descript_test.py", line 39, in __set__
raise Exception("score<0!")
Exception: score<0!

这里可以看到已经可以记录不同学生数据了。

实现缓存器

Flask使用的werkzeug中有用到描述器实现了一个缓存器。其在werkzeug.utils中。

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
class _Missing(object):
def __repr__(self):
return 'no value'

def __reduce__(self):
return '_missing'

_missing = _Missing()

class cached_property(property):
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func

def __set__(self, obj, value):
obj.__dict__[self.__name__] = value

def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.__name__, _missing)
if value is _missing:
value = self.func(obj)
obj.__dict__[self.__name__] = value
return value

使用示例:

1
2
3
4
5
6
7
8
9
class Foo(object):
@cached_property
def foo(self):
print 'foo test'
return 'test'

f = Foo()
print f.foo
print f.foo

输出:

1
2
3
foo test
test
test

上面可以看出如果该函数被调用过,再次调用的时候会直接去dict中找,从而达到缓存的目的。

内置property描述器

内置的property是常用于修饰函数,从而使其可以和实例属性一样访问的。该部分在Objects/descrobject.c中PyProperty_Type函数里面实现。

用法

下面是使用示例:

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
class Circle(object):
@property
def area(self):
return self.__r ** 2 * 3.14

@property
def r(self):
return self.__r

@r.setter
def r(self, r):
self.__r = r

def get_d(self):
return self.__r * 2

def set_d(self, d):
self.__r = d / 2

d = property(get_d, set_d)


if __name__ == '__main__':
c = Circle()
c.r = 1
print c.r
print c.area
print c.d
c.d = 4
print c.area

输出:

1
2
3
4
1
3.14
2
12.56

这里可以看出有两种方式使用property:
1、通过property装饰函数,即可用将函数变成属性访问的方式应用。
2、通过调用property函数,设置某一属性的get和set函数,即可在调用该属性时调用函数。

原理

由于property是通过C实现的,这里给出一个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
class Property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc

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

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

def __delete__(self, obj):
if self.fdel is None:
raise Exception("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__)

这里可以看到通过property修饰后访问的时候就会使得原有的函数名变成了一个property的实例,是一个描述器,在访问的时候就会查找到调用其__get__函数。