2019独角兽企业重金招聘Python工程师标准>>>

最近在阅读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.idpage = 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_onlydef __call__(self, func):functools.update_wrapper(self, func, updated=[])self.getter, self.key = func, self.key or func.__name__return selfdef __get__(self, obj, cls):if obj is None: return selfkey, 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] = valuedef __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 envrionself.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] = valuereturn 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] = valuereturn get

这样确实实现了解析查询字符串的功能,但每次在调用这个方法时都需要对原始查询字符串解析一次,实际上在处理某特请求时,查询字符串是不会改变的,所以我们只需要解析一次并把它保存起来,下次使用时直接返回就好了。另外此时的query方法还是一个普通方法,必须使用这样的方法来调用它

# 获取id
request.query().id
# 获取page
request.query().page

query后面的小括号让语句显得不那么协调,其实就是我觉得它丑。要是也能和官方文档中的示例实现以属性访问的方式获取相应的数据就好了。所以代码还得改改。

# query method version 2
class Request:"""some codes here"""@propertydef 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] = valuereturn 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 = attrself.key = keyself.getter = getterself.read_only = read_onlydef __set__(self, obj, value):if self.read_only:raise AttributeError('Read only property.')getattr(obj, self.attr)[self.key] = valuedef __get__(self, obj, cls):if obj is None:return selfkey, 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] = valuereturn 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_onlydef __call__(self, func):functools.update_wrapper(self, func, updated=[])self.getter, self.key = func, self.key or func.__name__return selfdef __get__(self, obj, cls):if obj is None: return selfkey, 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] = valuedef __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的各位有些帮助把。

转载于:https://my.oschina.net/leejun2005/blog/79918

Bottle 框架中的装饰器类和描述符应用相关推荐

  1. python类装饰器详解-python 中的装饰器详解

    装饰器 闭包 闭包简单的来说就是一个函数,在该函数内部再定义一个函数,并且这个内部函数用到了外部变量(即是外部函数的参数),最终这个函数返回内部函数的引用,这就是闭包. def decorator(p ...

  2. python类装饰器详解-Python类中的装饰器在当前类中的声明与调用详解

    我的Python环境:3.7 在Python类里声明一个装饰器,并在这个类里调用这个装饰器. 代码如下: class Test(): xx = False def __init__(self): pa ...

  3. python 类装饰器和函数装饰器区别_python进阶之装饰器之4在类中定义装饰器,将装饰器定义为类,两者的区别与联系...

    # 把装饰器定义为类 # 定义中需要实现__call__(),__get__() 方法 import types from functools import wraps class Profiled: ...

  4. python在类内部使用装饰器_python – 如何在类中使用装饰器

    我知道有类似的问题,但我的情况有些不同:参考代码: class MyClass(object): def __init__(self, log_location) self.logs = loggin ...

  5. 为什么在Python代码中需要装饰器

    Python is praised for its clarity and syntactic sugariness. In this article, I will teach you to use ...

  6. python中的装饰器怎么运行_Python 装饰器入门(上)

    翻译前想说的话: 这是一篇介绍python装饰器的文章,对比之前看到的类似介绍装饰器的文章,个人认为无人可出其右,文章由浅到深,由函数介绍到装饰器的高级应用,每个介绍必有例子说明.文章太长,看完原文后 ...

  7. python 类的使用(5)之类装饰器(类的装饰器和类作为装饰器)

    在阅读博客中,发现了类装饰器的存在,由于之前就在写类相关的专栏,这次就赶紧补上之前的内容啦.类装饰器这个词是有歧义的,因为类本身可以作为装饰器,一个类也可以被函数装饰器所装饰.今天就简单介绍一下这两种 ...

  8. python装饰器原理-python 中的装饰器及其原理

    装饰器模式 此前的文章中我们介绍过装饰器模式: 装饰器模式中具体的 Decorator 实现类通过将对组建的请求转发给被装饰的对象,并在转发前后执行一些额外的动作来修改原有的部分行为,实现增强 Com ...

  9. python装饰器 property_Python中@property装饰器的使用技巧性解析(代码示例)

    Python中@property装饰器的技巧性用法(代码示例) 本篇文章给大家带来的内容是关于Python中@property装饰器的技巧性用法(代码示例),有一定的参考价值,有需要的朋友可以参考一下 ...

最新文章

  1. fullpage的应用
  2. 遗传算法(GA)中的编码方式-二进制编码、格雷编码、实数编码
  3. [ARM异常]-ARMV8-aarch64 异常(中断)是如何跳转到向量表的
  4. c字符串中包含双引号_码哥学Python,一起解密神秘的字符串密码
  5. 如何对工业交换机端口进行调试?
  6. fscanf不读取_思考了一天,终于把matlab的fscanf中的sizeA搞清楚了
  7. Android实例-手机安全卫士(三十六)-根据Service是否开启确定CheckBox选中状态
  8. 【opencv学习】【Harris角点检测】
  9. lpc3250 TFT-4238液晶支持
  10. H3C交换机SSH配置
  11. stata 导出 相关系数表_【BBtime】戏说会计论文---stata简单实操
  12. 3D元素周期表源码(已加注释)及分析
  13. 看电影读小说,你就能懂经济学
  14. 向Android系统中添加模块及产品流程
  15. 20:球弹跳高度的计算
  16. iphone11夜景模式怎么开
  17. hadoop不在sudoers文件中。此事将被报告。 解决方法
  18. Canal-adapter的简单配置
  19. RFID射频识别电子标签基础知识汇总
  20. spark报错:CREATE TEMPORARY TABLE

热门文章

  1. swift变量和函数
  2. html中select标签刷新后不回到默认值而是保持之前选择值
  3. Nginx虚拟目录(转)
  4. WebUserControl归纳
  5. C# 多页打印简单实现
  6. Kotlin 基础语法
  7. GDI C++ 位图的绘制
  8. C#自定义ConfigSections节 操作 。
  9. java第十三章总结
  10. linux7有相当于wget的,centos7安装mwget下载资源,提升下载速度