• 技术文章 >Python技术 >Python高级

    Python中的描述符

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

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

    1

    2

    3

    4

    5

    6

    7

    8

    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

    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

    >>> 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

    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

    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

    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

    class Foo(object):

        @cached_property

        def foo(self):

            print 'Call me!'

            return 42

    调用下:

    1

    2

    3

    4

    5

    6

    7

    8

    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

    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

    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呢?当然可以,不过实现的要复杂很多:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    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

    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

    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

    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

    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)

    专题推荐:python
    上一篇:详解Python元类(metaclass) 下一篇:使用Python实现一个堆栈结构

    相关文章推荐

    • Python使用Pillow添加图片水印

    全部评论我要评论

    © 2021 Python学习网 苏ICP备2021003149号-1

  • 取消发布评论
  • 

    Python学习网