深入描述符

描述符是一种在多个属性上重复利用同一个存取逻辑的方式,他能”劫持”那些本对于self.dict的操作。描述符通常是一种包含getsetdelete三种方法中至少一种的类,给人的感觉是「把一个类的操作托付与另外一个类」。静态方法、类方法、property都是构建描述符的类。
我们先看一个简单的描述符的例子(基于我之前的分享的[Python高级编程](http://dongweiming.github.io/Expert-
Python/#43)改编,这个PPT建议大家去看看):

1
2
3
4
5
6
7
8
9
10
11
12

class MyDescriptor(object):
_value = ''
def __get__(self, instance, klass):
return self._value

def __set__(self, instance, value):
self._value = value.swapcase()


class Swap(object):
swap = MyDescriptor()

注意MyDescriptor要用新式类。调用一下:

1
2
3
4
5
6
7
8
9
10

In [1]: from descriptor_example import Swap
In [2]: instance = Swap()
In [3]: instance.swap # 没有报AttributeError错误,因为对swap的属性访问被描述符类重载了
Out[3]: ''
In [4]: instance.swap = 'make it swap' # 使用__set__重新设置_value
In [5]: instance.swap
Out[5]: 'MAKE IT SWAP'
In [6]: instance.__dict__ # 没有用到__dict__:被劫持了
Out[6]: {}

这就是描述符的威力。我们熟知的staticmethod、classmethod如果你不理解,那么看一下用Python实现的效果可能会更清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

>>> class myStaticMethod(object):
... def __init__(self, method):
... self.staticmethod = method
... def __get__(self, object, type=None):
... return self.staticmethod
...
>>> class myClassMethod(object):
... def __init__(self, method):
... self.classmethod = method
... def __get__(self, object, klass=None):
... if klass is None:
... klass = type(object)
... def newfunc(*args):
... return self.classmethod(klass, *args)
... return newfunc

在实际的生产项目中,描述符有什么用处呢?首先看MongoEngine中的Field的用法:

1
2
3
4
5
6
7
8
9
10
11

from mongoengine import *

class Metadata(EmbeddedDocument):
tags = ListField(StringField())
revisions = ListField(IntField())

class WikiPage(Document):
title = StringField(required=True)
text = StringField()
metadata = EmbeddedDocumentField(Metadata)

有非常多的Field类型,其实它们的基类就是一个描述符,我简化下,大家看看实现的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13

class BaseField(object):
name = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
...

def __get__(self, instance, owner):
return instance._data.get(self.name)

def __set__(self, instance, value):
...
instance._data[self.name] = value

很多项目的源代码看起来很复杂,在抽丝剥茧之后,其实原理非常简单,复杂的是业务逻辑。
接着我们再看Flask的依赖Werkzeug中的cached_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
29
30

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

class Foo(object):
@cached_property
def foo(self):
print 'Call me!'
return 42

调用下:

1
2
3
4
5
6
7
8
9
10
11

In [1]: from cached_property import Foo
...: foo = Foo()
...:

In [2]: foo.bar
Call me!
Out[2]: 42

In [3]: foo.bar
Out[3]: 42

可以看到在从第二次调用bar方法开始,其实用的是缓存的结果,并没有真的去执行。
说了这么多描述符的用法。我们写一个做字段验证的描述符:

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

class Quantity(object):
def __init__(self, name):
self.name = name

def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError('value must be > 0')


class Rectangle(object):
height = Quantity('height')
width = Quantity('width')

def __init__(self, height, width):
self.height = height
self.width = width

@property
def area(self):
return self.height * self.width

我们试一试:

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

In [1]: from rectangle import Rectangle
In [2]: r = Rectangle(10, 20)
In [3]: r.area
Out[3]: 200

In [4]: r = Rectangle(-1, 20)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-5-5a7fc56e8a> in <module>()
----> 1 r = Rectangle(-1, 20)

/Users/dongweiming/mp/2017-03-23/rectangle.py in __init__(self, height, width)

16 def __init__(self, height, width):
---> 17 self.height = height
18 self.width = width


/Users/dongweiming/mp/2017-03-23/rectangle.py in __set__(self, instance, value)
7 instance.__dict__[self.name] = value
8 else:
----> 9 raise ValueError('value must be > 0')



ValueError: value must be > 0

看到了吧,我们在描述符的类里面对传值进行了验证。ORM就是这么玩的!
但是上面的这个实现有个缺点,就是不太自动化,你看height = Quantity('height'),这得让属性和Quantity的name都叫做height,那么可不可以不用指定name呢?当然可以,不过实现的要复杂很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

class Quantity(object):
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.name = '_{}#{}'.format(prefix, index)
cls.__counter += 1

def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.name)
...


class Rectangle(object):
height = Quantity()
width = Quantity()
...

Quantity的name相当于类名+计时器,这个计时器每调用一次就叠加1,用此区分。有一点值得提一提,在get中的:

1
2
3

if instance is None:
return self

在很多地方可见,比如之前提到的MongoEngine中的BaseField。这是由于直接调用Rectangle.height这样的属性时候会报AttributeError,
因为描述符是实例上的属性。
PS:这个灵感来自《Fluent
Python》,书中还有一个我认为设计非常好的例子。就是当要验证的内容种类很多的时候,如何更好地扩展的问题。现在假设我们除了验证传入的值要大于0,还得验证不能为空和必须是数字(当然三种验证在一个方法中验证也是可以接受的,我这里就是个演示),我们先写一个abc的基类:

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

class Validated(abc.ABC):
__counter = 0

def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.name = '_{}#{}'.format(prefix, index)
cls.__counter += 1

def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.name)
def __set__(self, instance, value):
value = self.validate(instance, value)
setattr(instance, self.name, value)

@abc.abstractmethod
def validate(self, instance, value):
"""return validated value or raise ValueError"""

现在新加一个检查类型,新增一个继承了Validated的、包含检查的validate方法的类就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Quantity(Validated):
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value


class NonBlank(Validated):
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value

前面展示的描述符都是一个类,那么可不可以用函数来实现呢?也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

def quantity():
try:
quantity.counter += 1
except AttributeError:
quantity.counter = 0

storage_name = '_{}:{}'.format('quantity', quantity.counter)

def qty_getter(instance):
return getattr(instance, storage_name)

def qty_setter(instance, value):
if value > 0:
setattr(instance, storage_name, value)
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)

版权声明:本文由 董伟明 原创,未经作者授权禁止任何微信公众号和向掘金(juejin.im)转载,技术博客转载采用 保留署名-非商业性使用-禁止演绎 4.0-国际许可协议
python