纸上得来终觉浅,绝知此事要躬行。
描述符,有些地方也称为描述器。在开始讲之前,我们回忆一下之前博客文章中关于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
存在三个问题,如下所示。
- 这些获取、赋值以及删除的方法都写进了类里面,这种属性多了对应要写的方法就很多了,会让类非常臃肿。
- 这种属性都是存在
self.__dict__
里面的,比如directors
,都绑在了实例上,容易造成未来重构时可定制性很差。 - 属性对应的获取、赋值以及删除方法不容易被重复利用。
而描述符就是来解决这个问题的,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
就是mongdb
的ORM
,它的效果看起来是最直观的。
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))
其实对应的属性控制StringField
、DateTimeField
和ListField
,它们的基类都是来自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()