知行编程网知行编程网  2022-11-03 15:00 知行编程网 隐藏边栏  3 
文章评分 0 次,平均分 0.0
导语: 本文主要介绍了关于Python中的描述符的相关知识,包括连中符,以及中断描述符这些编程知识,希望对大家有参考作用。

描述符是一种在多个属性上重用相同访问逻辑的方法,它可以“劫持”原本会在 self.__dict__ 上执行的操作。描述符通常是一个类,其中至少包含__get__、__set__和__delete__三种方法中的一种,给人的印象是“将一个类的操作委托给另一个类”。静态方法、类方法和属性都是构建描述符的类。

Python 中的描述符

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

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要用新式类。调用一下:

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实现的效果可能会更清楚:

>>> 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的用法:

from mongoengine import *                      
class Metadata(EmbeddedDocument):                   
    tags = ListField(StringField())
    revisions = ListField(IntField())
class WikiPage(Document):                           
    title = StringField(required=True)              
    text = StringField()                            
    metadata = EmbeddedDocumentField(Metadata)

有很多字段类型。事实上,它们的基类是一个描述符。我会简化它。我们来看看实现原理:

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:

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

其实看类名就知道这是一个缓存属性。看不懂也没关系,用它:

class Foo(object):
    @cached_property
    def foo(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方法开始,实际使用的是缓存的结果,并没有实际执行。

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

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

我们试一试:

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的名字都叫height,所以可以不指定名字吗?当然可以,但是实现要复杂得多:

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的名字相当于类名+定时器。这个定时器每次调用都会叠加1,所以可以通过this来区分。值得一提的是,在 __get__ 中:

if instance is None:
    return self

在很多地方都可以看到,比如前面提到的MongoEngine中的BaseField。这是因为直接调用Rectangle.height等属性会报AttributeError,因为descriptor是实例上的一个属性。

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

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 方法的类:

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

上面显示的描述符都是类,那么可以用函数来实现吗?这也很好:

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)

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

知行编程网
知行编程网 关注:1    粉丝:1
这个人很懒,什么都没写
扫一扫二维码分享