深入描述符
描述符是一種在多個屬性上重複利用同一個存取邏輯的方式,他能"劫持"那些本對於self.__dict__的操作。描述符通常是一種包含__get__、__set__、__delete__三種方法中至少一種的類,給人的感覺是「把一個類的操作託付與另外一個類」。靜態方法、類方法、property都是構建描述符的類。
我們先看一個簡單的描述符的例子(基於我之前的分享的Python高級編程改編,這個PPT建議大家去看看):
class MyDescriptor(object):n _value = n def __get__(self, instance, klass):n return self._valuen def __set__(self, instance, value):n self._value = value.swapcase()nclass Swap(object):n swap = MyDescriptor()n
注意MyDescriptor要用新式類。調用一下:
In [1]: from descriptor_example import SwapnIn [2]: instance = Swap()nIn [3]: instance.swap # 沒有報AttributeError錯誤,因為對swap的屬性訪問被描述符類重載了nOut[3]: nIn [4]: instance.swap = make it swap # 使用__set__重新設置_valuenIn [5]: instance.swapnOut[5]: MAKE IT SWAPnIn [6]: instance.__dict__ # 沒有用到__dict__:被劫持了nOut[6]: {}n
這就是描述符的威力。我們熟知的staticmethod、classmethod如果你不理解,那麼看一下用Python實現的效果可能會更清楚了:
>>> class myStaticMethod(object):n... def __init__(self, method):n... self.staticmethod = methodn... def __get__(self, object, type=None):n... return self.staticmethodn...n>>> class myClassMethod(object):n... def __init__(self, method):n... self.classmethod = methodn... def __get__(self, object, klass=None):n... if klass is None:n... klass = type(object)n... def newfunc(*args):n... return self.classmethod(klass, *args)n... return newfuncn
在實際的生產項目中,描述符有什麼用處呢?首先看MongoEngine中的Field的用法:
from mongoengine import * nclass Metadata(EmbeddedDocument): n tags = ListField(StringField())n revisions = ListField(IntField())nclass WikiPage(Document): n title = StringField(required=True) n text = StringField() n metadata = EmbeddedDocumentField(Metadata)n
有非常多的Field類型,其實它們的基類就是一個描述符,我簡化下,大家看看實現的原理:
class BaseField(object):n name = Nonen def __init__(self, **kwargs):n self.__dict__.update(kwargs)n ...n def __get__(self, instance, owner):n return instance._data.get(self.name)n def __set__(self, instance, value):n ...n instance._data[self.name] = valuen
很多項目的源代碼看起來很複雜,在抽絲剝繭之後,其實原理非常簡單,複雜的是業務邏輯。
接著我們再看Flask的依賴Werkzeug中的cached_property:
class _Missing(object):n def __repr__(self):n return no valuen def __reduce__(self):n return _missingnnn_missing = _Missing() nnnclass cached_property(property):n def __init__(self, func, name=None, doc=None):n self.__name__ = name or func.__name__n self.__module__ = func.__module__n self.__doc__ = doc or func.__doc__n self.func = funcn def __set__(self, obj, value):n obj.__dict__[self.__name__] = valuen def __get__(self, obj, type=None):n if obj is None:n return selfn value = obj.__dict__.get(self.__name__, _missing)n if value is _missing:n value = self.func(obj)n obj.__dict__[self.__name__] = valuen return valuen
其實看類的名字就知道這是緩存屬性的,看不懂沒關係,用一下:
class Foo(object):n @cached_propertyn def foo(self):n print Call me!n return 42n
調用下:
In [1]: from cached_property import Foon ...: foo = Foo()n ...:nIn [2]: foo.barnCall me!nOut[2]: 42nIn [3]: foo.barnOut[3]: 42n
可以看到在從第二次調用bar方法開始,其實用的是緩存的結果,並沒有真的去執行。
說了這麼多描述符的用法。我們寫一個做欄位驗證的描述符:
class Quantity(object):n def __init__(self, name):n self.name = namen def __set__(self, instance, value):n if value > 0:n instance.__dict__[self.name] = valuen else:n raise ValueError(value must be > 0)nnnclass Rectangle(object):n height = Quantity(height)n width = Quantity(width)n def __init__(self, height, width):n self.height = heightn self.width = widthn @propertyn def area(self):n return self.height * self.widthn
我們試一試:
In [1]: from rectangle import RectanglenIn [2]: r = Rectangle(10, 20)nIn [3]: r.areanOut[3]: 200nIn [4]: r = Rectangle(-1, 20)n---------------------------------------------------------------------------nValueError Traceback (most recent call last)n<ipython-input-5-5a7fc56e8a> in <module>()n----> 1 r = Rectangle(-1, 20)n/Users/dongweiming/mp/2017-03-23/rectangle.py in __init__(self, height, width)n 15n 16 def __init__(self, height, width):n---> 17 self.height = heightn 18 self.width = widthn 19n/Users/dongweiming/mp/2017-03-23/rectangle.py in __set__(self, instance, value)n 7 instance.__dict__[self.name] = valuen 8 else:n----> 9 raise ValueError(value must be > 0)n 10n 11nValueError: value must be > 0n
看到了吧,我們在描述符的類裡面對傳值進行了驗證。ORM就是這麼玩的!
但是上面的這個實現有個缺點,就是不太自動化,你看 height =Quantity(height),這得讓屬性和Quantity的name都叫做height,那麼可不可以不用指定name呢?當然可以,不過實現的要複雜很多:
class Quantity(object):n __counter = 0n def __init__(self):n cls = self.__class__n prefix = cls.__name__n index = cls.__countern self.name = _{}#{}.format(prefix, index)n cls.__counter += 1n def __get__(self, instance, owner):n if instance is None:n return selfn return getattr(instance, self.name)n ...nnnclass Rectangle(object):n height = Quantity()n width = Quantity() n ...n
Quantity的name相當於類名+計時器,這個計時器每調用一次就疊加1,用此區分。有一點值得提一提,在__get__中的:
if instance is None:n return selfn
在很多地方可見,比如之前提到的MongoEngine中的BaseField。這是由於直接調用Rectangle.height這樣的屬性時候會報AttributeError, 因為描述符是實例上的屬性。
PS:這個靈感來自《Fluent Python》,書中還有一個我認為設計非常好的例子。就是當要驗證的內容種類很多的時候,如何更好地擴展的問題。現在假設我們除了驗證傳入的值要大於0,還得驗證不能為空和必須是數字(當然三種驗證在一個方法中驗證也是可以接受的,我這裡就是個演示),我們先寫一個abc的基類:
class Validated(abc.ABC):n __counter = 0n def __init__(self):n cls = self.__class__n prefix = cls.__name__n index = cls.__countern self.name = _{}#{}.format(prefix, index)n cls.__counter += 1n def __get__(self, instance, owner):n if instance is None:n return selfn else:n return getattr(instance, self.name)n def __set__(self, instance, value):n value = self.validate(instance, value)n setattr(instance, self.name, value) n @abc.abstractmethodn def validate(self, instance, value):n """return validated value or raise ValueError"""n
現在新加一個檢查類型,新增一個繼承了Validated的、包含檢查的validate方法的類就可以了:
class Quantity(Validated):n def validate(self, instance, value):n if value <= 0:n raise ValueError(value must be > 0)n return valuenclass NonBlank(Validated):n def validate(self, instance, value):n value = value.strip()n if len(value) == 0:n raise ValueError(value cannot be empty or blank)n return valuen
前面展示的描述符都是一個類,那麼可不可以用函數來實現呢?也是可以的:
def quantity():n try:n quantity.counter += 1n except AttributeError:n quantity.counter = 0n storage_name = _{}:{}.format(quantity, quantity.counter)n def qty_getter(instance):n return getattr(instance, storage_name)n def qty_setter(instance, value):n if value > 0:n setattr(instance, storage_name, value)n else:n raise ValueError(value must be > 0)n return property(qty_getter, qty_setter)n
這些都掌握了,你也就算熟悉描述符啦,加油??!
PS:本文全部代碼可以在微信公眾號文章代碼庫項目(dongweiming/mp)中找到。
無恥的廣告:《Python Web開發實戰》上市了!
歡迎關注本人的微信公眾號獲取更多Python相關的內容(也可以直接搜索「Python之美」):
推薦閱讀:
※pandas複習總結(二)
※Python數據分析之鎖具裝箱問題
TAG:Python |