Python描述符


纸上得来终觉浅,绝知此事要躬行。

Python描述符


描述符,有些地方也称为描述器。在开始讲之前,我们回忆一下之前博客文章中关于property的使用方法的介绍。通过这样的用法,就可以控制directors这个属性。而这个property就是将对象属性的获取、赋值、删除等行为,从方法转变成了一个属性。

class Movie:
    kind = 'movie'

    def__init__(self, id, category_id, title, directors=[]):
        self._directors = directors

    def get_directors(self):
        return self._directors

    def set_directors(self, value):
        if not isinstance(value, list):
            raise ValueError('invalid type')
        self._directors = value

    def del_directors(self):
        print('del')

    directors = property(get_directors, set_directors, del_directors)
In [1]: movie = Movie(4, 1002, '电影2', ['导演1'])

In [2]: movie.directorsOut: ['导演1']

In [3]: movie.directors = ['导演3']

In [4]: movie.directorsOut: ['导演3']

In [5]: movie.directors = None
---------------------------------------------------------------------------
ValueError                                Traceback (most recent calllast)
<ipython-input-52-109ff7dc9307> in <module>()
----> 1 movie.directors = None

<ipython-input-47-7314804d846e> in set_directors(self, value)
     10    def set_directors(self, value):
     11        if not isinstance(value, list):
---> 12             raise ValueError('invalid type')
     13         self._directors = value
     14

ValueError: invalid type

但是,使用property存在三个问题,如下所示。

  1. 这些获取、赋值以及删除的方法都写进了类里面,这种属性多了对应要写的方法就很多了,会让类非常臃肿。
  2. 这种属性都是存在self.__dict__里面的,比如directors,都绑在了实例上,容易造成未来重构时可定制性很差。
  3. 属性对应的获取、赋值以及删除方法不容易被重复利用。

而描述符就是来解决这个问题的,Python描述符是一种创建托管属性的方法。它是一种在多个属性上重复利用同一个存取逻辑的方式,他能劫持那些本对于self.__dict__的操作。描述符通常是一种包含__get____set____delete__三种方法中至少一种的类,给人的感觉是「把一个类的操作托付与另外一个类」。静态方法、类方法、property都是构建描述符的类。

从下面的示例中,我们可以看出__get__是用于访问属性的并返回属性的值,而这个__set__就是设置属性的值且并不会返回任何的值,而__delete__就是控制删除的操作。

classMyDescriptor:
     _value = ''

     def __get__(self, instance, klass):
         returnself._value

     def __set__(self, instance, value):
         self._value = value.swapcase()

     def__delete__(self, instance):
         print(f'Deleting: {self._value}')


class Swap:
     swap = MyDescriptor()
In [1]: inst = Swap()

# 没有报错,因为对swap的属性访问被描述符__get__重载了
In [2]: inst.swap
Out[2]:''

# 使用设置描述符__set__重新了_value的值
In [3]: inst.swap = 'make it swap'

In [4]: inst.swap
Out[4]:'MAKE IT SWAP'

# 这里并没有用到__dict__命名空间,其被劫持了
In [5]: inst.__dict__
Out[5]: {}

In [6]: del inst.swap
Deleting: MAKE IT SWAP
  • Python 实现静态方法
class myStaticMethod:
   def __init__(self, method):
       self.staticmethod = method

   def __get__(self, object, type=None):
       return self.staticmethod
  • Python 实现类方法
class myClassMethod:
    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
  • Python 实现 property 方法
class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is notNone:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
  • 描述符的实际用途

那在我们实际的使用项目生产中,描述符有什么作用呢?日常工作中,我们最常见的就是ORM了。这个mongoengine就是mongdbORM,它的效果看起来是最直观的。

from mongoengine import *
connect('mydb')

class BlogPost(Document):
    title = StringField(required=True, max_length=200)
    posted = DateTimeField(default=datetime.datetime.utcnow)
    tags = ListField(StringField(max_length=50))

其实对应的属性控制StringFieldDateTimeFieldListField,它们的基类都是来自BaseField的。而BaseField就是使用了描述符,这里我们简化之后理解一下。

class BaseField:
    name = None

    def __init__(self, name, **kwargs):
        self.name = nameself.__dict__.update(kwargs)
        # 其他逻辑

    def __get__(self, instance, owner):
    return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        # 其他逻辑
        instance.__dict__[self.name] = value
class Blog:
    # 能不能写成title = BaseField()
    title = BaseField(name='title')
    slug = BaseField(name='slug')

其实很多开源项目的源代码都看起来很复杂,只是需要我们抽丝剥茧,将其原理展现出来,这样看起来就非常简单的。而复杂的是那些业务逻辑和各种判断处理。

上述代码其实有一个问题,那就是我们不能直接使用title = BaseField()指定,而必须给其传递name=xxx。这是为了让实例里面的属性(title)和描述符对应的属性要一一对应上。那么可不可以不指定属性,而让其自动去获取呢?

这个当然是可以的,但是描述符需要注意,复用的时候需要互相不能有影响。上面的示例中,如果有多次BaseField,就无法分辨是从哪里属性来的。在《流畅的Python》这本书中有对应的写法,这里不写出来了。

而在Python3.6中,描述符的协议做了一点改进,现在可以直接实现这样的功能了。注意这里多了一个__set_name__的魔法方法,这个魔法方法在描述符创建的时候会被调用,可以指定到对应的属性上。这样就可以,将对象属性(title)和描述符属性(self.name)有一个对应关系。

class BaseField:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        # 其他逻辑

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        # 其他逻辑
        instance.__dict__[self.name] = value
In [1]: classBlog:
   ...:     title = BaseField()
   ...:     slug = BaseField()

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !