Python中使用装饰器时需要注意的一些问题


Posted in Python onMay 11, 2015

装饰器基本概念

大家都知道装饰器是一个很著名的设计模式,经常被用于AOP(面向切面编程)的场景,较为经典的有插入日志,性能测试,事务处理,Web权限校验,Cache等。

Python语言本身提供了装饰器语法(@),典型的装饰器实现如下:

  

@function_wrapper
  def function():
    pass

@实际上是python2.4才提出的语法糖,针对python2.4以前的版本有另一种等价的实现:

def function():
    pass

  function = function_wrapper(function)

装饰器的两种实现

函数包装器 - 经典实现

   

def function_wrapper(wrapped):
    def _wrapper(*args, **kwargs):
      return wrapped(*args, **kwargs)
    return _wrapper 

  @function_wrapper
  def function():
    pass

类包装器 - 易于理解

 

class function_wrapper(object):
    def __init__(self, wrapped):
      self.wrapped = wrapped
    def __call__(self, *args, **kwargs):
      return self.wrapped(*args, **kwargs)

  @function_wrapper
  def function():
    pass

函数(function)自省

当我们谈到一个函数时,通常希望这个函数的属性像其文档上描述的那样,是被明确定义的,例如__name__和__doc__ 。

针对某个函数应用装饰器时,这个函数的属性就会发生变化,但这并不是我们所期望的。

  

def function_wrapper(wrapped):
    def _wrapper(*args, **kwargs):
      return wrapped(*args, **kwargs)
    return _wrapper 

  @function_wrapper
  def function():
    pass 

  >>> print(function.__name__)
  _wrapper

python标准库提供了functools.wraps(),来解决这个问题。

import functools 

  def function_wrapper(wrapped):
    @functools.wraps(wrapped)
    def _wrapper(*args, **kwargs):
      return wrapped(*args, **kwargs)
    return _wrapper 

  @function_wrapper
  def function():
    pass 

  >>> print(function.__name__)
  function

然而,当我们想要获取被包装函数的参数(argument)或源代码(source code)时,同样不能得到我们想要的结果。

import inspect 

  def function_wrapper(wrapped): ...

  @function_wrapper
  def function(arg1, arg2): pass 

  >>> print(inspect.getargspec(function))
  ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)

  >>> print(inspect.getsource(function))
    @functools.wraps(wrapped)
    def _wrapper(*args, **kwargs):
      return wrapped(*args, **kwargs)

包装类方法(@classmethod)

当包装器(@function_wrapper)被应用于@classmethod时,将会抛出如下异常:

  

class Class(object):
    @function_wrapper
    @classmethod
    def cmethod(cls):
      pass 

  Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
   File "<stdin>", line 3, in Class
   File "<stdin>", line 2, in wrapper
   File ".../functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
  AttributeError: 'classmethod' object has no attribute '__module__'

因为@classmethod在实现时,缺少functools.update_wrapper需要的某些属性。这是functools.update_wrapper在python2中的bug,3.2版本已被修复,参考http://bugs.python.org/issue3445。

然而,在python3下执行,另一个问题出现了:

   

class Class(object):
    @function_wrapper
    @classmethod
    def cmethod(cls):
      pass 

  >>> Class.cmethod() 
  Traceback (most recent call last):
   File "classmethod.py", line 15, in <module>
    Class.cmethod()
   File "classmethod.py", line 6, in _wrapper
    return wrapped(*args, **kwargs)
  TypeError: 'classmethod' object is not callable

这是因为包装器认定被包装的函数(@classmethod)是可以直接被调用的,但事实并不一定是这样的。被包装的函数实际上可能是描述符(descriptor),意味着为了使其可调用,该函数(描述符)必须被正确地绑定到某个实例上。关于描述符的定义,可以参考https://docs.python.org/2/howto/descriptor.html。
总结 - 简单并不意味着正确

尽管大家实现装饰器所用的方法通常都很简单,但这并不意味着它们一定是正确的并且始终能正常工作。

如同上面我们所看到的,functools.wraps()可以帮我们解决__name__和__doc__ 的问题,但对于获取函数的参数(argument)或源代码(source code)则束手无策。

以上问题,wrapt都可以帮忙解决,详细用法可参考其官方文档:http://wrapt.readthedocs.org

Python 相关文章推荐
Python实现从url中提取域名的几种方法
Sep 26 Python
Python 字典dict使用介绍
Nov 30 Python
Python实现将罗马数字转换成普通阿拉伯数字的方法
Apr 19 Python
python 列表删除所有指定元素的方法
Apr 19 Python
Django框架实现逆向解析url的方法
Jul 04 Python
pygame游戏之旅 如何制作游戏障碍
Nov 20 Python
详解pandas安装若干异常及解决方案总结
Jan 10 Python
详解Python是如何实现issubclass的
Jul 24 Python
python实现读取类别频数数据画水平条形图案例
Apr 24 Python
keras模型保存为tensorflow的二进制模型方式
May 25 Python
自学python用什么系统好
Jun 23 Python
python微信智能AI机器人实现多种支付方式
Apr 12 Python
python在linux系统下获取系统内存使用情况的方法
May 11 #Python
Python实现登录人人网并抓取新鲜事的方法
May 11 #Python
python实现中文输出的两种方法
May 09 #Python
python使用xlrd实现检索excel中某列含有指定字符串记录的方法
May 09 #Python
Python遍历指定文件及文件夹的方法
May 09 #Python
Python使用chardet判断字符编码
May 09 #Python
python操作ie登陆土豆网的方法
May 09 #Python
You might like
php实现MySQL数据库备份与还原类实例
2014/12/09 PHP
PHP那些琐碎的知识点(整理)
2017/05/20 PHP
PHP通过GD库实现验证码功能示例
2019/02/23 PHP
jQuery chili图片远处放大插件
2009/11/30 Javascript
jquery中dom操作和事件的实例学习 仿yahoo邮箱登录框的提示效果
2011/11/30 Javascript
JavaScript检测并限制复选框选中个数的方法
2015/08/12 Javascript
js实现拉幕效果的广告代码
2015/09/02 Javascript
javascript常见数字进制转换实例分析
2016/04/21 Javascript
js返回顶部实例分享
2016/12/21 Javascript
JavaScript实现翻页功能(附效果图)
2017/02/16 Javascript
vue项目中使用axios上传图片等文件操作
2017/11/02 Javascript
vue实现随机验证码功能的实例代码
2019/04/30 Javascript
Vue.extend 登录注册模态框的实现
2020/12/29 Vue.js
Eclipse + Python 的安装与配置流程
2013/03/05 Python
Python只用40行代码编写的计算器实例
2017/05/10 Python
对pyqt5中QTabWidget的相关操作详解
2019/06/21 Python
Mac 使用python3的matplot画图不显示的解决
2019/11/23 Python
python3格式化字符串 f-string的高级用法(推荐)
2020/03/04 Python
Eclipse配置python默认头过程图解
2020/04/26 Python
Python Celery异步任务队列使用方法解析
2020/08/10 Python
CSS3动画之利用requestAnimationFrame触发重新播放功能
2019/09/11 HTML / CSS
New Balance天猫官方旗舰店:始于1906年,百年慢跑品牌
2017/11/15 全球购物
英国领先的电子、技术和办公用品购物网站:Ebuyer
2018/04/04 全球购物
新加坡一家在线男士皮具品牌:Faire Leather Co.
2019/12/01 全球购物
linux面试题参考答案(1)
2016/01/22 面试题
大学生简历的个人自我评价
2013/12/04 职场文书
医院护士专业个人的求职信
2013/12/09 职场文书
家长给孩子的评语
2014/01/30 职场文书
商铺租赁意向书
2014/04/01 职场文书
个人贷款担保书
2014/04/01 职场文书
《少年王勃》教学反思
2014/04/27 职场文书
安康杯竞赛活动总结
2014/05/05 职场文书
2014年学前班工作总结
2014/12/08 职场文书
MySQL复制问题的三个参数分析
2021/04/07 MySQL
Mysql 如何查询时间段交集
2021/06/08 MySQL
Python实现简单的俄罗斯方块游戏
2021/09/25 Python