描述符是一种在多个属性上重复利用同一个存取逻辑的方式,他能"劫持"那些本对于self.__dict__的操作。描述符通常是一种包含__get__、__set__、__delete__三种方法中至少一种的类,给人的感觉是「把一个类的操作托付与另外一个类」。静态方法、类方法、property都是构建描述符的类。

Python中的描述符

我们先看一个简单的描述符的例子:

classMyDescriptor(object):
_value=''
def__get__(self,instance,klass):
returnself._value
def__set__(self,instance,value):
self._value=value.swapcase()
classSwap(object):
swap=MyDescriptor()

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

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

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

>>>classmyStaticMethod(object):
...def__init__(self,method):
...self.staticmethod=method
...def__get__(self,object,type=None):
...returnself.staticmethod
...
>>>classmyClassMethod(object):
...def__init__(self,method):
...self.classmethod=method
...def__get__(self,object,klass=None):
...ifklassisNone:
...klass=type(object)
...defnewfunc(*args):
...returnself.classmethod(klass,*args)
...returnnewfunc

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

frommongoengineimport*
classMetadata(EmbeddedDocument):
tags=ListField(StringField())
revisions=ListField(IntField())
classWikiPage(Document):
title=StringField(required=True)
text=StringField()
metadata=EmbeddedDocumentField(Metadata)

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

classBaseField(object):
name=None
def__init__(self,**kwargs):
self.__dict__.update(kwargs)
...
def__get__(self,instance,owner):
returninstance._data.get(self.name)
def__set__(self,instance,value):
...
instance._data[self.name]=value

很多项目的源代码看起来很复杂,在抽丝剥茧之后,其实原理非常简单,复杂的是业务逻辑。

接着我们再看Flask的依赖Werkzeug中的cached_property:

class_Missing(object):
def__repr__(self):
return'novalue'
def__reduce__(self):
return'_missing'
_missing=_Missing()
classcached_property(property):
def__init__(self,func,name=None,doc=None):
self.__name__=nameorfunc.__name__
self.__module__=func.__module__
self.__doc__=docorfunc.__doc__
self.func=func
def__set__(self,obj,value):
obj.__dict__[self.__name__]=value
def__get__(self,obj,type=None):
ifobjisNone:
returnself
value=obj.__dict__.get(self.__name__,_missing)
ifvalueis_missing:
value=self.func(obj)
obj.__dict__[self.__name__]=value
returnvalue

其实看类的名字就知道这是缓存属性的,看不懂没关系,用一下:

classFoo(object):
@cached_property
deffoo(self):
print'Callme!'
return42

调用下:

In[1]:fromcached_propertyimportFoo
...:foo=Foo()
...:
In[2]:foo.bar
Callme!
Out[2]:42
In[3]:foo.bar
Out[3]:42

可以看到在从第二次调用bar方法开始,其实用的是缓存的结果,并没有真的去执行。

说了这么多描述符的用法。我们写一个做字段验证的描述符:

classQuantity(object):
def__init__(self,name):
self.name=name
def__set__(self,instance,value):
ifvalue>0:
instance.__dict__[self.name]=value
else:
raiseValueError('valuemustbe>0')
classRectangle(object):
height=Quantity('height')
width=Quantity('width')
def__init__(self,height,width):
self.height=height
self.width=width
@property
defarea(self):
returnself.height*self.width

我们试一试:

In[1]:fromrectangleimportRectangle
In[2]:r=Rectangle(10,20)
In[3]:r.area
Out[3]:200
In[4]:r=Rectangle(-1,20)
---------------------------------------------------------------------------
ValueErrorTraceback(mostrecentcalllast)
<ipython-input-5-5a7fc56e8a>in<module>()
---->1r=Rectangle(-1,20)
/Users/dongweiming/mp/2017-03-23/rectangle.pyin__init__(self,height,width)
15
16def__init__(self,height,width):
--->17self.height=height
18self.width=width
19
/Users/dongweiming/mp/2017-03-23/rectangle.pyin__set__(self,instance,value)
7instance.__dict__[self.name]=value
8else:
---->9raiseValueError('valuemustbe>0')
10
11
ValueError:valuemustbe>0

看到了吧,我们在描述符的类里面对传值进行了验证。ORM就是这么玩的!

但是上面的这个实现有个缺点,就是不太自动化,你看height = Quantity('height'),这得让属性和Quantity的name都叫做height,那么可不可以不用指定name呢?当然可以,不过实现的要复杂很多:

classQuantity(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):
ifinstanceisNone:
returnself
returngetattr(instance,self.name)
...
classRectangle(object):
height=Quantity()
width=Quantity()
...

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

ifinstanceisNone:
returnself

在很多地方可见,比如之前提到的MongoEngine中的BaseField。这是由于直接调用Rectangle.height这样的属性时候会报AttributeError, 因为描述符是实例上的属性。

PS:这个灵感来自《Fluent Python》,书中还有一个我认为设计非常好的例子。就是当要验证的内容种类很多的时候,如何更好地扩展的问题。现在假设我们除了验证传入的值要大于0,还得验证不能为空和必须是数字(当然三种验证在一个方法中验证也是可以接受的,我这里就是个演示),我们先写一个abc的基类:

classValidated(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):
ifinstanceisNone:
returnself
else:
returngetattr(instance,self.name)
def__set__(self,instance,value):
value=self.validate(instance,value)
setattr(instance,self.name,value)
@abc.abstractmethod
defvalidate(self,instance,value):
"""returnvalidatedvalueorraiseValueError"""

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

classQuantity(Validated):
defvalidate(self,instance,value):
ifvalue<=0:
raiseValueError('valuemustbe>0')
returnvalue
classNonBlank(Validated):
defvalidate(self,instance,value):
value=value.strip()
iflen(value)==0:
raiseValueError('valuecannotbeemptyorblank')
returnvalue
defquantity():
try:
quantity.counter+=1
exceptAttributeError:
quantity.counter=0
storage_name='_{}:{}'.format('quantity',quantity.counter)
defqty_getter(instance):
returngetattr(instance,storage_name)
defqty_setter(instance,value):
ifvalue>0:
setattr(instance,storage_name,value)
else:
raiseValueError('valuemustbe>0')
returnproperty(qty_getter,qty_setter)

发表回复