阿布云

你所需要的,不仅仅是一个好用的代理。

描述符的简单了解

阿布云 发表于

28.png

描述符是一种在多个属性上重复利用同一个存取逻辑的方式,他能”劫持”那些本对于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)