最近在阅读Python微型Web框架Bottle的源码,发现了Bottle中有一个既是装饰器类又是描述符的有趣实现。恰好这两个点是Python比较的难理解,又混合在一块儿,让代码有些晦涩难懂。但理解代码以后不禁得为Python语言的简洁优美赞叹。因此把相关知识和想法稍微整理,以供分享。编程
Bottle是Python的一个微型Web框架,全部代码都在一个bottle.py文件中,只依赖标准库实现,兼容Python 2和Python 3,并且最新的稳定版0.12代码也只有3700行左右。虽然小,但它实现了Web框架基本功能。这里就不以过多的笔墨去展现Bottle框架,须要的请访问其网站了解更多。这里着重介绍与本文相关的重要对象request。在Bottle里,request对象表明了当前线程处理的请求,客户端发送的请求数据如表单数据,请求网站和cookie均可以从request对象中得到。下面是官方文档中的两个例子
from bottle import request, route, response, templatecookie
# 获取客户端cookie以实现登录时问候用户功能 @route('/hello') def hello(): name = request.cookie.username or 'Guest' return template('Hello {{name}}', name=name) # 获取形如/forum?id=1&page=5的查询字符串中id和page变量的值 route('/forum') def display_forum(): forum_id = request.query.id page = request.query.page or '1' return template('Forum ID: {{id}} (page {{page}})', id=forum_id, page=page)
那么Bottle是如何实现的呢?根据WSGI接口规定,全部的HTTP请求信息都包含在一个名为envrion的dict对象中。因此Bottle要作的就是把HTTP请求信息从environ解析出来。在深刻Request类如何实现以前先要了解下Bottle的FormsDict。FormsDict与字典类类似,但扩展了一些功能,好比支持属性访问、一对多的键值对、WTForms支持等。它在Bottle中被普遍应用,如上面的示例中cookie和query数据都以FormsDict存储,因此咱们能够用request.query.page的方式获取相应属性值。
下面是0.12版Bottle中Request类的部分代码,0.12版中Request类继承了BaseRequest,为了方便阅读我把代码合并在一块儿,同时还有重要的DictProperty的代码。须要说明的是Request类__init__传入的environ参数就是WSGI协议中包含HTTP请求信息的envrion,而query方法中的_parse_qsl函数能够接受形如/forum?id=1&page=5原始查询字符串而后以[(key1, value1), (ke2, value2), ...]的list返回。闭包
class DictProperty(object): """ Property that maps to a key in a local dict-like attribute. """ def __init__(self, attr, key=None, read_only=False): self.attr, self.key, self.read_only = attr, key, read_only def __call__(self, func): functools.update_wrapper(self, func, updated=[]) self.getter, self.key = func, self.key or func.__name__ return self def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __set__(self, obj, value): if self.read_only: raise AttributeError("Read-Only property.") getattr(obj, self.attr)[self.key] = value def __delete__(self, obj): if self.read_only: raise AttributeError("Read-Only property.") del getattr(obj, self.attr)[self.key] class Request: def __init__(self, environ=None): self.environ {} if environ is None else envrion self.envrion['bottle.request'] = self @DictProperty('environ', 'bottle.request.query', read_only=True) def query(self): get = self.environ['bottle.get'] = FormsDict() pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) for key, value in pairs: get[key] = value return get
query方法的逻辑和代码都比较简单,就是从environ中获取'QUERY_STRING',并用把原始查询字符串解析为一个FormsDict,将这个FormsDict赋值给environ['bottle.request.query']并返回。但这个函数的装饰器的做用就有些难以理解,装饰器的实现方式都是"dunder"特殊方法,有些晦涩难懂。若是上来就看这些源码可能难以理解代码实现的功能。那不如这些放一边,假设本身要实现这些方法,你会写出什么代码。
一开始你可能写出这样的代码。app
# version 1 class Request: """ some codes here """ def query(self): get = self.environ['bottle.get'] = FormsDict() pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) for key, value in pairs: get[key] = value return get
这样确实实现了解析查询字符串的功能,但每次在调用这个方法时都须要对原始查询字符串解析一次,实际上在处理某特请求时,查询字符串是不会改变的,因此咱们只须要解析一次并把它保存起来,下次使用时直接返回就行了。另外此时的query方法仍是一个普通方法,必须使用这样的方法来调用它框架
# 获取id request.query().id # 获取page request.query().page
query后面的小括号让语句显得不那么协调,其实就是我以为它丑。要是也能和官方文档中的示例实现以属性访问的方式获取相应的数据就行了。因此代码还得改改。函数
# query method version 2 class Request: """ some codes here """ @property def query(self): if 'bootle.get.query' not in self.environ: get = self.environ['bottle.get'] = FormsDict() pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) for key, value in pairs: get[key] = value return self.environ['bottle.get.query']
第二版改变的代码就两处,一个是使用property装饰器,实现了request.query的访问方式;另外一个就是在query函数体中增长了判断'bottle.get.query'是否在environ中的判断语句,实现了只解析一次的要求。第二版几乎知足了全部要求,它表现得就像Bottle中真正的query方法同样。但它仍是有些缺陷。
首先,Request类并不仅有query一个方法,若是要编写完整的Request类就会发现,有不少方法的代码与query类似,都是从environ中解析出须要的数据,并且都只须要解析一次,保存起来,第二次或之后访问时返回保存的数据就行了。因此能够考虑将属性管理的代码从方法体内抽象出来,正好Python中的描述符能够实现这样的功能。另外若是使用Bottle的开发者在写代码时不当心尝试进行request.query = some_data的赋值时,将会抛出以下错误。网站
>>> AttributeError: can't set attribute
咱们确实但愿属性是只读的,在对其赋值时应该抛出错误,但这样的报错信息并无提供太多有用的信息,致使调bug时一头雾水,找不到方向。咱们更但愿抛出如线程
>>> AttributeError: Read-only property
这样明确的错误信息。
因此第三版的代码能够这样写调试
# query method version 3 class Descriptor: def __init__(self, attr, key, getter, read_only=False): self.attr = attr self.key = key self.getter = getter self.read_only = read_only def __set__(self, obj, value): if self.read_only: raise AttributeError('Read only property.') getattr(obj, self.attr)[self.key] = value def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __delete__(self, obj): if self.read_only: raise AttributeError('Read only property.') del getattr(obj, self.attr)[self.key] class Reqeust: """ some codes """ def query(self): get = self.environ['bottle.get'] = FormsDict() pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) for key, value in pairs: get[key] = value return get query = Descriptor('environ', 'bottle.get.query', query, read_only=True)
第三版的代码没有使用property装饰器,而是使用了描述符这个技巧。若是你以前没有见到过描述符,在这里限于篇幅只能作个简单的介绍,但描述符涉及知识点众多,若是有不清楚之处能够看看《流畅的Python》第20章属性描述符,里面有很是详细的介绍。
简单来讲,描述符是对多个属性运用相同存取逻辑的一种方式,如Bottle框架里咱们须要对不少属性都进行判断某个键是否在environ中,若是在则返回,若是不在,须要解析一次这样的存取逻辑。而描述符须要实现特定协议,包括__set__, __get__, __delete___方法,分别对应设置,读取和删除属性的方法。他么的参数也比较特殊,如__get__方法的三个参数self, obj, cls分别对应描述符实例的引用,对第三版的代码来讲就是Descriptor('environ', 'bottle.get.query', query, read_only=True)建立的实例的引用;obj则对应将某个属性托管给描述的实例对象的引用,对应的应该为request对象;而cls则为Request类的引用。在调用request.query时编译器会自动传入这些参数。若是以Request.query的方式调用,那么obj参数的传入值为None,这时候一般的处理是返回描述符实例。
在Descriptor中__get__方法的代码最多,也比较难理解,但若是记住其参数的意义也没那么难。下面以query的实现为例,我添加一些注释来帮助理解code
key, storage = self.key, getattr(obj, self.attr) # key='bottle.get.query' # storage = environ 即包含HTTP请求的信息的environ # 判断envrion中是否包含key来决定是否须要解析 if key not in storage: storage[key] = self.getter(obj) # self.getter(obj)就是调用了原来的query方法,不过要传入一个Request实例,也就是obj return storage[key]
而__set__, __delete__代码比较简单,在这里咱们把只读属性在赋值和删除时抛出的错误定制为AttributeError('Read only property.'),方便调试。
经过使用描述符这个有些难懂的方法,咱们能够在Request的方法中专心于编写如何解析的代码,不用担忧属性的存取逻辑。和在每一个方法中都使用if判断相比高到不知道哪里去。但美中不足的是,这样让咱们的方法代码后面拖着一个“小尾巴”,即
query = Descriptor('envrion', 'bottle.get.query', query, read_only=True)
怎么去掉这个这个“小尾巴“呢?回顾以前的代码几乎都是对query之类的方法进行修饰,因此能够尝试使用装饰器,毕竟装饰器就是对某个函数进行修饰的,并且咱们应该使用参数化的装饰器,这样才能将envrion等参数传递给装饰器。若是要实现参数化装饰器就须要一个装饰器工厂函数,也就是说装饰器的代码里须要嵌套至少3个函数体,写起来有写绕,代码可阅读性也有差。更大的问题来自如何将描述符与装饰器结合起来,由于Descriptor是一个类而不是方法。
解决办法其实挺简单的。若是知道Python中函数也是对象,实现了__call__方法的对象能够表现得像函数同样。因此咱们能够修改Descirptor的代码,实现__call__方法,让它的实例成为callable对象就能够把它用做装饰器;而要传入的参数能够以实例属性存储起来,经过self.attribute的形式访问,而不是像使用工厂函数实现参数化装饰器时经过闭包来实现参数的访问获取。这时候再来看看Bottle里的DictProperty代码
class DictProperty(object): """ Property that maps to a key in a local dict-like attribute. """ def __init__(self, attr, key=None, read_only=False): self.attr, self.key, self.read_only = attr, key, read_only def __call__(self, func): functools.update_wrapper(self, func, updated=[]) self.getter, self.key = func, self.key or func.__name__ return self def __get__(self, obj, cls): if obj is None: return self key, storage = self.key, getattr(obj, self.attr) if key not in storage: storage[key] = self.getter(obj) return storage[key] def __set__(self, obj, value): if self.read_only: raise AttributeError("Read-Only property.") getattr(obj, self.attr)[self.key] = value def __delete__(self, obj): if self.read_only: raise AttributeError("Read-Only property.") del getattr(obj, self.attr)[self.key]
其实就是一个有描述符做用的装饰器类,它的使用方法很简单:
@DictProperty('environ', 'bottle.get.query', read_only=True) def query(self): """ some codes """
拆开会更好理解点:
property = DictProperty('environ', 'bottle.get.query', read_only=True) @property def query(self): """ some codes """
再把@实现的语法糖拆开:
def query(self): """ some codes """
property = DictProperty('environ', 'bottle.get.query', read_only=True)
query = property(query) # @实现的语法糖
再修改如下代码形式:
def query(self): """ some codes """
query = DictProperty('environ', 'bottle.get.query', read_only=True)(query)
是否是和第三版的实现方式很是类似。
def query(self): """ some codes """ query = Descriptor('environ', 'bottle.get.query', query, read_only=True)
但咱们可使用装饰器把方法体后面那个不和谐的赋值语句”小尾巴“去掉,将属性存取管理抽象出来,并且只须要使用一行很是简便的装饰器把这个功能添加到某个方法上。这也许就是Python的美之一吧。
DictProperty涉及知识远不止文中涉及的那么简单,若是你仍是不清楚DictProperty的实现功能,建议阅读《流畅的Python》第7章和第22章,对装饰器和描述符有详细的描述,另外《Python Cookbook》第三版第9章元编程有关于参数化装饰器和装饰器类的叙述和示例。若是你对Bottle为何要实现这样的功能感到困惑,建议阅读Bottle的文档和WSGI相关的文章。其实前一阵再阅读Bottle源码时就想写一篇文章,但奈何许久不写东西文笔生疏加上医院实习期间又比较忙,一直推到如今才终于磕磕绊绊地把我阅读的Bottle源码的一些感悟写出来,但愿对喜欢Python的各位有些帮助把。