Bottle框架中的装饰器类和描述符应用详解


Posted in Python onOctober 28, 2017

最近在阅读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, template

# 获取客户端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”特殊方法,有些晦涩难懂。如果上来就看这些源码可能难以理解代码实现的功能。那不如这些放一边,假设自己要实现这些方法,你会写出什么代码。

一开始你可能写出这样的代码。

# 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的实现为例,我添加一些注释来帮助理解

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的各位有些帮助把。

总结

以上就是本文关于Bottle框架中的装饰器类和描述符应用详解的全部内容,希望对大家有所帮助。感谢朋友们对本站的支持!

Python 相关文章推荐
python实现网页链接提取的方法分享
Feb 25 Python
Python多进程编程技术实例分析
Sep 16 Python
Python实现的一个简单LRU cache
Sep 26 Python
python之PyMongo使用总结
May 26 Python
python WindowsError的错误代码详解
Jul 23 Python
django+mysql的使用示例
Nov 23 Python
python pandas时序处理相关功能详解
Jul 03 Python
Python实现的爬取豆瓣电影信息功能案例
Sep 15 Python
简单了解python filter、map、reduce的区别
Jan 14 Python
python Yaml、Json、Dict之间的转化
Oct 19 Python
python 下载文件的多种方法汇总
Nov 17 Python
如何基于Python pygame实现动画跑马灯
Nov 18 Python
Pandas探索之高性能函数eval和query解析
Oct 28 #Python
Python探索之URL Dispatcher实例详解
Oct 28 #Python
Python探索之Metaclass初步了解
Oct 28 #Python
Python编程之Re模块下的函数介绍
Oct 28 #Python
Python探索之静态方法和类方法的区别详解
Oct 27 #Python
Python探索之爬取电商售卖信息代码示例
Oct 27 #Python
Python 列表理解及使用方法
Oct 27 #Python
You might like
深入array multisort排序原理的详解
2013/06/18 PHP
smarty模板中使用get、post、request、cookies、session变量的方法
2014/04/24 PHP
PHP 导出Excel示例分享
2014/08/18 PHP
php中stdClass的用法分析
2015/02/27 PHP
CI框架入门之MVC简单示例
2016/11/21 PHP
php命名空间设计思想、用法与缺点分析
2019/07/17 PHP
javascript 学习笔记(一)DOM基本操作
2011/04/08 Javascript
jQuery 文本框得失焦点的简单实例
2014/02/19 Javascript
jQuery插件Zclip实现完美兼容个浏览器点击复制内容到剪贴板
2015/04/30 Javascript
vue制作加载更多功能的正确打开方式
2016/10/12 Javascript
js实现音频控制进度条功能
2017/04/01 Javascript
Angular 2父子组件之间共享服务通信的实现
2017/07/04 Javascript
深入理解Node module模块
2018/03/26 Javascript
微信小程序防止多次点击跳转和防止表单组件输入内容多次验证功能(函数防抖)
2019/09/19 Javascript
微信小程序吸底区域适配iPhoneX的实现
2020/04/09 Javascript
javascript实现图片轮换动作方法
2020/08/07 Javascript
[01:31:03]DOTA2完美盛典全回顾 见证十五项大奖花落谁家
2017/11/28 DOTA
新手该如何学python怎么学好python?
2008/10/07 Python
对于Python装饰器使用的一些建议
2015/06/03 Python
Python安装图文教程 Pycharm安装教程
2018/03/27 Python
Django forms组件的使用教程
2018/10/08 Python
Python线性拟合实现函数与用法示例
2018/12/13 Python
django多个APP的urls设置方法(views重复问题解决)
2019/07/19 Python
python 列表、字典和集合的添加和删除操作
2019/12/16 Python
Python selenium抓取虎牙短视频代码实例
2020/03/02 Python
Python读取二进制文件代码方法解析
2020/06/22 Python
白宫黑市官网:White House Black Market
2016/11/17 全球购物
文员个人求职自荐信
2013/09/21 职场文书
行政助理岗位职责范文
2013/12/03 职场文书
白血病捐款倡议书
2014/05/14 职场文书
文明和谐家庭事迹材料
2014/05/18 职场文书
教育局党的群众路线教育实践活动整改方案
2014/09/20 职场文书
2015年入党积极分子评语
2015/03/26 职场文书
任命通知范文
2015/04/21 职场文书
nginx实现发布静态资源的方法
2021/03/31 Servers
MySQL 隔离数据列和前缀索引的使用总结
2021/05/14 MySQL