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

最近在阅读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框架中的装饰器类和描述符应用详解的全部内容,希望对大家有所帮助。感谢朋友们对本站的支持!

(0)

相关推荐

  • 利用Python的装饰器解决Bottle框架中用户验证问题

    首先来分析下需求,web程序后台需要认证,后台页面包含多个页面,最普通的方法就是为每个url添加认证,但是这样就需要每个每个绑定url的后台函数都需要添加类似或者相同的代码,但是这样做代码就过度冗余,而且不利于扩展. 接下来我们先不谈及装饰器,我们都知道Python是个很强大的语言,她可以将函数当做参数传递给函数,最简单的: def p(): print 'Hello,world' def funcfactor(func): print 'calling function named', fun

  • 简单的连接MySQL与Python的Bottle框架的方法

    Python关于mySQL的连接插件众多,Bottle下也有人专门开发的插件:bottle-mysql具体使用方法见官方,总共感觉其用法限制太多,其使用起来不方便,最适合的当然是,mySQL官网给Python提供的通用官方驱动,用起来很顺手:mysql-connector  具体操作如下: # -*- coding: utf-8 -*- #!/usr/bin/python # filename: login_admin.py # codedtime: 2014-9-7 11:26:11 impo

  • Python的Bottle框架中获取制定cookie的教程

    这两天为用bottle+mongodb写的一个项目加上登录功能,无奈怎么都获取不到保存的cookie,文档给出让我们这样操作cookie的代码片段: @route('/login') def login (): username = request .forms .get('username ') password = request .forms .get('password ') if check_user_credentials(username, password): response

  • Python的Bottle框架的一些使用技巧介绍

    之前对bottle做过不少的介绍,也写过一些文章来说明bottle的缺点,最近发现其实之前有些地方说的不太公平,所以趁此机会也来更正一下. bottle是支持类似flask url_for的语法的,具体使用方法在下文介绍     bottle的request.query之类的参数默认是str类型,也是有原因的,比如我在给google做代理的时候,编码就不一定是utf8的,如果强制转化utf8就会报错     之前的bug也得到了修正,比如mount('/x',app)之后,/x/和/x都可以访问

  • Python的Bottle框架中返回静态文件和JSON对象的方法

    代码如下: # -*- coding: utf-8 -*- #!/usr/bin/python # filename: todo.py # codedtime: 2014-8-28 20:50:44 import sqlite3 import bottle @bottle.route('/help3') def help(): return bottle.static_file('help.html', root='.') #静态文件 @bottle.route('/json:json#[0-9

  • Python的Bottle框架中实现最基本的get和post的方法的教程

    1.GET方式: # -*- coding: utf-8 -*- #!/usr/bin/python # filename: GETPOST_test.py # codedtime: 2014-9-20 19:07:04 import bottle def check_login(username, password): if username == '123' and password == '234': return True else: return False @bottle.route

  • 在Python的Bottle框架中使用微信API的示例

    微信这个东西估计宅男没几个不熟悉的吧,微信经过这么两年多的发展终于向开放平台跨出了友好的一步.蛋疼的以为微信会出一个详细的api等接口,兴奋不已的去申请了微信公共平台,然后开始找各种api的位置-- 花费了近一个小时,依然没找到-- 最后动用Google大杀器,终于找到了这么个链接.我了个去的,没比这还简单的api文档了吧. 最让人无法理解的是:居然没有本地开发环境支持,每次都要放在生产环境去调试. 最让人欣慰的是:就那么俩方法,生产环境调试几次也就完事了. Python(bottle)版代码如

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

    最近在阅读Python微型Web框架Bottle的源码,发现了Bottle中有一个既是装饰器类又是描述符的有趣实现.刚好这两个点是Python比较的难理解,又混合在一起,让代码有些晦涩难懂.但理解代码之后不由得为Python语言的简洁优美赞叹.所以把相关知识和想法稍微整理,以供分享. 正文 Bottle是Python的一个微型Web框架,所有代码都在一个bottle.py文件中,只依赖标准库实现,兼容Python 2和Python 3,而且最新的稳定版0.12代码也只有3700行左右.虽然小,但

  • Laravel框架中队列和工作(Queues、Jobs)操作实例详解

    在我们的web应用中,经常会遇到这样的情况: 用户在进行了某项操作后,我们需要在后台完成一个耗时且耗费资源的任务,以对应用户的操作. 通常来说,web应用中的操作都是同步的(synchronous),即用户的操作可以立即得到回馈. 但是在以上情况下,同步等待操作结果将是灾难性的.比如用户点击了申请密码重置邮件,倘若我们让用户一直停滞在等待页面,直至邮件发送成功,那么用户体验将非常地不好,因为有时候可能需要很长的时间才能将邮件发送完成. 从另一个角度来说,如果我们服务器处于高负荷的情况,当多个用户

  • 应用市场中Java拦截器和切面的使用实例详解

    相信大家对拦截器和切面的概念已经不陌生了,本文我们就看一些在应用市场中,拦截器和切面的使用是怎样的. 拦截器的使用:每次接收到某个请求之前,都会调用此拦截器中的方法,其中preHandle方法如果return true,表示继续调用对应的controller,如果return false, public class CheckLoginInterceptor implements HandlerInterceptor { private Logger logger = Logger.getLog

  • c++中的单例类模板的实现方法详解

     1.什么是单例模式 在架构设计时,某些类在整个系统生命周期中最多只能有一个对象存在 ( Single Instance ).如超市收银系统,其外观主要由显示器(1个).扫描枪(1个).收款箱(1个)组成,在系统正常运行期间这三部分都是唯一存在的:也就是说,显示器.扫描枪.收款箱这三部分都应该有各自的类,并且每个类只能唯一地实例化一个对象,基于这种模式的程序设计,称为单例模式. !!!单例模式只能创建一个对象,且该对象的生命周期伴随系统的整个运行期间. 2.怎么实现单例模式 思考:如何定义一个类

  • Python装饰器实现方法及应用场景详解

    应用场景: 1.授权(Authorization) 装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint).它们被大量使用于Flask和Django web框架中.这里是一个例子来使用基于装饰器的授权: from functools import wraps # 最新版python引用是 import functools def requires_auth(f): # f 就是我们需要装饰的函数,一看就是不带参数的装饰器 @wraps(f) # 新版python写法 @

  • Python 装饰器原理、定义与用法详解

    本文实例讲述了Python 装饰器原理.定义与用法.分享给大家供大家参考,具体如下: Python 装饰器 一.何为装饰器 1.在函数中定义函数 在函数中定义另外的函数,就是说可以创建嵌套的函数,例子如下 def sayHi(name="hjj2"): print 'inside sayHi() func' def greet(): return 'inside greet() func' print(greet()) sayHi() #output # inside sayHi()

  • Python的装饰器模式与面向切面编程详解

    今天来讨论一下装饰器.装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理等.装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用.概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能. 1. 装饰器入门 1.1. 需求是怎么来的? 装饰器的定义很是抽象,我们来看一个小例子. 复制代码 代码如下: def foo():     print 'in foo()'   foo() 这是一个很无聊

  • Java中的拦截器、过滤器、监听器用法详解

    本文实例讲述了Java中的拦截器.过滤器.监听器用法.分享给大家供大家参考,具体如下: 一.拦截器 :是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方 法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作. 1.Struts2拦截器是在访问某个Action或Action的某个方法,字段之前或之后实施拦截,并且Struts2拦截器是可插拔的,

  • SSM框架中测试单元的使用 spring整合Junit过程详解

    测试类中的问题和解决思路 问题 在测试类中,每个测试方法都有以下两行代码: ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); IAccountService as = ac.getBean("accountService",IAccountService.class); 这两行代码的作用是获取容器,如果不写的话,直接会提示空指针异常.所以又不能轻易删掉. 解决思路分析 针对

  • Java的MyBatis框架中关键的XML字段映射的配置参数详解

    properties 这些是外部化的,可替代的属性,这些属性也可以配置在典型的Java属性配置文件中,或者通过properties元素的子元素来传递.例如: <properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="pas

随机推荐