深入描述符
描述符是一種在多個屬性上重復利用同一個存取邏輯的方式,他能”劫持”那些本對于self.__dict__的操作。描述符通常是一種包含__get__、__set__、__delete__三種方法中至少一種的類,給人的感覺是「把一個類的操作托付與另外一個類」。靜態方法、類方法、property都是構建描述符的類。
我們先看一個簡單的描述符的例子(基于我之前的分享的 Python高級編程 改編,這個PPT建議大家去看看):
classMyDescriptor(object):
_value = ''
def__get__(self, instance, klass):
return self._value
def__set__(self, instance, value):
self._value = value.swapcase()
classSwap(object):
swap = MyDescriptor()
注意MyDescriptor要用新式類。調用一下:
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實現的效果可能會更清楚了:
>>> classmyStaticMethod(object):
... def__init__(self, method):
... self.staticmethod = method
... def__get__(self, object, type=None):
... return self.staticmethod
...
>>> classmyClassMethod(object):
... def__init__(self, method):
... self.classmethod = method
... def__get__(self, object, klass=None):
... if klass is None:
... klass = type(object)
... defnewfunc(*args):
... return self.classmethod(klass, *args)
... return newfunc
在實際的生產項目中,描述符有什么用處呢?首先看MongoEngine中的Field的用法:
from mongoengine import *
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):
return instance._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 'no value'
def__reduce__(self):
return '_missing'
_missing = _Missing()
classcached_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
其實看類的名字就知道這是緩存屬性的,看不懂沒關系,用一下:
classFoo(object):
@cached_property
deffoo(self):
print 'Call me!'
return 42
調用下:
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方法開始,其實用的是緩存的結果,并沒有真的去執行。
說了這么多描述符的用法。我們寫一個做字段驗證的描述符:
classQuantity(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')
classRectangle(object):
height = Quantity('height')
width = Quantity('width')
def__init__(self, height, width):
self.height = height
self.width = width
@property
defarea(self):
return self.height * self.width
我們試一試:
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)
15
16 def __init__(self, height, width):
---> 17 self.height = height
18 self.width = width
19
/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')
10
11
ValueError: value must be > 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):
if instance is None:
return self
return getattr(instance, self.name)
...
classRectangle(object):
height = Quantity()
width = Quantity()
...
Quantity的name相當于類名+計時器,這個計時器每調用一次就疊加1,用此區分。有一點值得提一提,在__get__中的:
if instance is None:
return self
在很多地方可見,比如之前提到的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):
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
defvalidate(self, instance, value):
"""return validated value or raise ValueError"""
現在新加一個檢查類型,新增一個繼承了Validated的、包含檢查的validate方法的類就可以了:
classQuantity(Validated):
defvalidate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value
classNonBlank(Validated):
defvalidate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
前面展示的描述符都是一個類,那么可不可以用函數來實現呢?也是可以的:
defquantity():
try:
quantity.counter += 1
except AttributeError:
quantity.counter = 0
storage_name = '_{}:{}'.format('quantity', quantity.counter)
defqty_getter(instance):
return getattr(instance, storage_name)
defqty_setter(instance, value):
if value > 0:
setattr(instance, storage_name, value)
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
來自:http://www.dongwm.com/archives/深入屬性描述符/