Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5119830
  • 博文数量: 921
  • 博客积分: 16037
  • 博客等级: 上将
  • 技术积分: 8469
  • 用 户 组: 普通用户
  • 注册时间: 2006-04-05 02:08
文章分类

全部博文(921)

文章存档

2020年(1)

2019年(3)

2018年(3)

2017年(6)

2016年(47)

2015年(72)

2014年(25)

2013年(72)

2012年(125)

2011年(182)

2010年(42)

2009年(14)

2008年(85)

2007年(89)

2006年(155)

分类: Python/Ruby

2015-06-30 10:10:45

《A Byte of Python》17.8节讲decorator的时候,用到了functools模块中的一个装饰器:wraps。因为之前没有接触过这个装饰器,所以特地研究了一下。

何谓“装饰器”?

《A Byte of Python》中这样讲:

“Decorators are a shortcut to applying wrapper functions. This is helpful to “wrap” functionality with the same code over and over again.”

《Python参考手册(第4版)》6.5节描述如下:

“装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。”

Python官方文档中这样定义:

“A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod().”

让我们来看一下《Python参考手册》上6.5节的一个例子(有些许改动):

  1. # coding: utf-8
  2. # Filename: decorator_wraps_test.py
  3. # 2014-07-05 18:58
  4. import sys

  5. debug_log = sys.stderr

  6. def trace(func):
  7.     if debug_log:
  8.         def callf(*args, **kwargs):
  9.             """A wrapper function."""
  10.             debug_log.write('Calling function: {}\n'.format(func.__name__))
  11.             res = func(*args, **kwargs)
  12.             debug_log.write('Return value: {}\n'.format(res))
  13.             return res
  14.         return callf
  15.     else:
  16.         return func

  17. @trace
  18. def square(x):
  19.     """Calculate the square of the given number."""
  20.     return x * x

  21. if __name__ == '__main__':
  22.     print(square(3))

输出:
Calling function: square
Return value: 9
9

这个例子中,我们定义了一个装饰器trace,用于追踪函数的调用过程及函数调用的返回值。如果不用装饰器语法,我们也可以这样写:


  1. def _square(x):
  2.     return x * x

  3. square = trace(_square)

上面两段代码,使用装饰器语法的版本和不用装饰器语法的版本实际上是等效的。只是当我们使用装饰器时,我们不必再手动调用装饰器函数。

嗯。trace装饰器看起来棒极了!假设我们把如上代码提供给其他程序员使用,他可能会想看一下square函数的帮助文档:


  1. >>> from decorator_wraps_test import square
  2. >>> help(square) # print(square.__doc__)
  3. Help on function callf in module decorator_wraps_test:
  4. callf(*args, **kwargs)
  5. A wrapper function.

看到这样的结果,使用decorator_wraps_test.py模块的程序员一定会感到困惑。他可能会带着疑问敲入如下代码:


  1. >>> print(square.__name__)
  2. callf

这下,他可能会想看一看decorator_wraps_test.py的源码,找一找问题究竟出现在了哪里。我们知道,Python中所有对象都是“第 一类”的。比如,函数(对象),我们可以把它当作普通的数据对待:我们可以把它存储到容器中,或者作为另一个函数的返回值。上面的程序中,在 debug_log为真的情况下,trace会返回一个函数对象callf。这个函数对象就是一个“闭包”,因为当我们通过:


  1. def _square(x): return x * x
  2. square = trace(_square)

把trace返回的callf存储到square时,我们得到的不仅仅是callf函数执行语句,还有其上下文环境:


  1. >>> print('debug_log' in square.__globals__)
  2. True
  3. >>> print('sys' in square.__globals__)
  4. True

因此,使用装饰器修饰过的函数square,实际上是一个trace函数返回的“闭包”对象callf,这就揭示了上面help(square)以及print(square.__name__)的输出结果了。

那么,怎样才能在使用装饰器的基础上,还能让help(square)及print(square.__name__)得到我们期待的结果呢?这就是functools模块的wraps装饰器的作用了。

让我们先看一看效果:


  1. # coding: utf-8
  2. # Filename: decorator_wraps_test.py
  3. # 2014-07-05 18:58
  4. import functools
  5. import sys

  6. debug_log = sys.stderr

  7. def trace(func):
  8.     if debug_log:
  9.         @functools.wraps(func)
  10.         def callf(*args, **kwargs):
  11.             """A wrapper function."""
  12.             debug_log.write('Calling function: {}\n'.format(func.__name__))
  13.             res = func(*args, **kwargs)
  14.             debug_log.write('Return value: {}\n'.format(res))
  15.             return res
  16.         return callf
  17.     else:
  18.         return func

  19. @trace
  20. def square(x):
  21.     """Calculate the square of the given number."""
  22.     return x * x

  23. if __name__ == '__main__':
  24.     print(square(3))
  25.     print(square.__doc__)
  26.     print(square.__name__)

输出:

Calling function: square
Return value: 9
9
Calculate the square of the given number.
square

很完美!哈哈。这里,我们使用了一个带参数的wraps装饰器“装饰”了嵌套函数callf,得到了预期的效果。那么,wraps的原理是什么呢?

首先,简要介绍一下带参数的装饰器:



  1. >>> def trace(log_level):
  2. def impl_f(func):
  3. print(log_level, 'Implementing function: "{}"'.format(func.__name__))
  4. return func
  5. return impl_f
  6. >>> @trace('[INFO]')
  7. def print_msg(msg): print(msg)
  8. [INFO] Implementing function: "print_msg"
  9. >>> @trace('[DEBUG]')
  10. def assert_(expr): assert expr
  11. [DEBUG] Implementing function: "assert_"
  12. >>> print_msg('Hello, world!')
  13. Hello, world!

这段代码定义了一个带参数的trace装饰器函数。因此:


  1. @trace('[INFO]')
  2. def print_msg(msg): print(msg)
等价于:

  1. temp = trace('[INFO]')
  2. def _print_msg(msg): print(msg)
  3. print_msg = temp(_print_msg)

相信这样类比一下,带参数的装饰器就很好理解了。(当然,这个例子举得并不好。《Python参考手册》上有一个关于带参数的装饰器的更好的例子,感兴趣的童鞋可以自己看看 。)

接下来,让我们看看wraps这个装饰器的代码吧!

让我们先找到functools模块文件的路径:


  1. >>> import functools
  2. >>> functools.__file__
  3. 'D:\\Program Files\\Python34\\lib\\functools.py'
下面,把wraps相关的代码摘录出来:


  1. WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
  2.                        '__annotations__')
  3. WRAPPER_UPDATES = ('__dict__',)
  4. def update_wrapper(wrapper,
  5.                    wrapped,
  6.                    assigned = WRAPPER_ASSIGNMENTS,
  7.                    updated = WRAPPER_UPDATES):
  8.     """Update a wrapper function to look like the wrapped function

  9.        wrapper is the function to be updated
  10.        wrapped is the original function
  11.        assigned is a tuple naming the attributes assigned directly
  12.        from the wrapped function to the wrapper function (defaults to
  13.        functools.WRAPPER_ASSIGNMENTS)
  14.        updated is a tuple naming the attributes of the wrapper that
  15.        are updated with the corresponding attribute from the wrapped
  16.        function (defaults to functools.WRAPPER_UPDATES)
  17.     """
  18.     for attr in assigned:
  19.         try:
  20.             value = getattr(wrapped, attr)
  21.         except AttributeError:
  22.             pass
  23.         else:
  24.             setattr(wrapper, attr, value)
  25.     for attr in updated:
  26.         getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
  27.     # Issue #17482: set __wrapped__ last so we don

从代码中可以看到,wraps是通过functools模块中另外两个函数:partial和update_wrapper来实现其功能的。让我们看一看这两个函数:

1. partial函数

partial函数实现对函数参数进行部分求值(《Python参考手册》中4.9有这么一句:函数参数的部分求值与叫做柯里化(currying)的过程关系十分密切。虽然不是太明白,但感觉很厉害的样子!2014-07-07 15:05追加内容:在百度博客中,zotin大哥回复了我,并对函数式编程中柯里化概念做了一些解释。):


  1. >>> from functools import partial
  2. >>> def foo(x, y, z):
  3. print(locals())
  4. >>> foo(1, 2, 3)
  5. {'z': 3, 'y': 2, 'x': 1}
  6. >>> foo_without_z = partial(foo, z = 100)
  7. >>> foo_without_z
  8. functools.partial(<function foo at 0x00000000033FC6A8>, z=100)
  9. >>> foo_without_z is foo
  10. False
  11. >>> foo_without_z(10, 20)
  12. {'z': 100, 'y': 20, 'x': 10}

这里,我们通过partial为foo提供参数z的值,得到了一个新的“函数对象”(这里之所以加个引号是因为foo_without_z和一般的函数对象有些差别。比如,foo_without_z没有__name__属性。)foo_without_z。因此,本例中:


  1. foo_without_z(10, 20)
等价于:


  1. foo(10, 20, z = 100)

(比较有趣的一点是,foo_without_z没有__name__属性,而其文档字符串__doc__也和partial的文档字符串很相像。此外, 我认为,这里的partial和C++标准库中的bind1st、bind2nd这些parameter binders有异曲同工之妙。这里没有把partial函数的实现代码摘录出来,有兴趣的童鞋可以自己研究一下它的工作原理。)

因此,wraps函数中:


  1. return partial(update_wrapper, wrapped=wrapped,
  2.                    assigned=assigned, updated=updated)
实际上是返回一个对update_wrapper进行部分求值的“函数对象”。因此,上例中使用了wraps装饰器的decorator_wraps_test.py的等价版本如下:


  1. def trace(func):
  2.     if debug_log:
  3.         def _callf(*args, **kwargs):
  4.             """A wrapper function."""
  5.             debug_log.write('Calling function: {}\n'.format(func.__name__))
  6.             res = func(*args, **kwargs)
  7.             debug_log.write('Return value: {}\n'.format(res))
  8.             return res

  9.         _temp = functools.wraps(func)
  10.         callf = _temp(_callf)
  11.         return callf
  12.     else:
  13.         return func
对wraps也进行展开:


  1. def trace(func):
  2.     if debug_log:
  3.         def _callf(*args, **kwargs):
  4.             """A wrapper function."""
  5.             debug_log.write('Calling function: {}\n'.format(func.__name__))
  6.             res = func(*args, **kwargs)
  7.             debug_log.write('Return value: {}\n'.format(res))
  8.             return res

  9.         _temp = functools.partial(functools.update_wrapper,
  10.                                   wrapped = func,
  11.                                   assigned = functools.WRAPPER_ASSIGNMENTS,
  12.                                   updated = functools.WRAPPER_UPDATES)
  13.         callf = _temp(_callf)
  14.         return callf
  15.     else:
  16.         return func

最后,对partial的调用也进行展开:


  1. def trace(func):
  2.     if debug_log:
  3.         def _callf(*args, **kwargs):
  4.             """A wrapper function."""
  5.             debug_log.write('Calling function: {}\n'.format(func.__name__))
  6.             res = func(*args, **kwargs)
  7.             debug_log.write('Return value: {}\n'.format(res))
  8.             return res

  9.         callf = functools.update_wrapper(_callf,
  10.                                          wrapped = func,
  11.                                          assigned = functools.WRAPPER_ASSIGNMENTS,
  12.                                          updated = functools.WRAPPER_UPDATES)

  13.         return callf
  14.     else:
  15.         return func

这次,我们看到的是很直观的函数调用:用_callf和func作为参数调用update_wrapper函数。

2. update_wrapper函数

update_wrapper做的工作很简单,就是用参数wrapped表示的函数对象(例如:square)的一些属性(如:__name__、 __doc__)覆盖参数wrapper表示的函数对象(例如:callf,这里callf只是简单地调用square函数,因此可以说callf是 square的一个wrapper function)的这些相应属性。

因此,本例中使用wraps装饰器“装饰”过callf后,callf的__doc__、__name__等属性和trace要“装饰”的函数square的这些属性完全一样。

经过上面的分析,相信你也了解了functools.wraps的作用了吧。

最后,《A Byte of Python》一书讲装饰器的时候提到了一篇博客:DRY Principles through Python Decorators 。有兴趣的童鞋可以去阅读以下。



原文链接


阅读(1402) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~