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操作mysql中文显示乱码的解决方法
Oct 11 Python
Python实现抓取页面上链接的简单爬虫分享
Jan 21 Python
详解在Python和IPython中使用Docker
Apr 28 Python
在Python的Django框架下使用django-tagging的教程
May 30 Python
在Python的Django框架中加载模版的方法
Jul 16 Python
Python随机生成带特殊字符的密码
Mar 02 Python
Python中selenium实现文件上传所有方法整理总结
Apr 01 Python
利用python将图片转换成excel文档格式
Dec 30 Python
Python3使用pandas模块读写excel操作示例
Jul 03 Python
Django框架自定义session处理操作示例
May 27 Python
python之信息加密题目详解
Jun 26 Python
在python中画正态分布图像的实例
Jul 08 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
PHP输出控制功能在简繁体转换中的应用
2006/10/09 PHP
header()函数使用说明
2006/11/23 PHP
php实现压缩多个CSS与JS文件的方法
2014/11/11 PHP
PHP文件操作详解
2016/12/30 PHP
php中文语义分析实现方法示例
2019/09/28 PHP
Ucren Virtual Desktop V2.0
2006/11/07 Javascript
用最通俗易懂的代码帮助新手理解javascript闭包 推荐
2012/03/01 Javascript
如何将JS的变量值传递给ASP变量
2012/12/10 Javascript
node.js中的console.info方法使用说明
2014/12/09 Javascript
jQuery中;function($,undefined) 前面的分号的用处
2014/12/17 Javascript
js实现同一页面多个不同运动效果的方法
2015/04/10 Javascript
JavaScript添加随滚动条滚动窗体的方法
2016/02/23 Javascript
巧用canvas
2017/01/21 Javascript
js数字滑动时钟的简单实现(示例讲解)
2017/08/14 Javascript
使用Nodejs连接mongodb数据库的实现代码
2017/08/21 NodeJs
vue中的数据绑定原理的实现
2018/07/02 Javascript
详解Element-UI中上传的文件前端处理
2019/08/07 Javascript
Python3中configparser模块读写ini文件并解析配置的用法详解
2020/02/18 Python
python中Array和DataFrame相互转换的实例讲解
2021/02/03 Python
用HTML5制作一个简单的桌球游戏的教程
2015/05/12 HTML / CSS
h5封装下拉刷新
2020/08/25 HTML / CSS
乌克兰机票、铁路和巴士票、酒店搜索、保险:Tickets.ua
2020/01/11 全球购物
Java提供了哪些企业应用编程接口
2015/02/13 面试题
银行财务部实习生的自我鉴定
2013/11/27 职场文书
运动会广播稿80字
2014/01/23 职场文书
护士毕业生自我鉴定
2014/02/08 职场文书
超市仓管员岗位职责
2014/04/07 职场文书
三年级学生评语
2014/04/23 职场文书
医学生求职信
2014/07/01 职场文书
考试作弊被抓检讨书
2014/10/02 职场文书
党员四风自我剖析材料
2014/10/07 职场文书
家长会开场白和结束语
2015/05/29 职场文书
地道战观后感500字
2015/06/04 职场文书
浅谈MySQL表空间回收的正确姿势
2021/10/05 MySQL
一文弄懂MySQL索引创建原则
2022/02/28 MySQL
mysql 子查询的使用
2022/04/28 MySQL