探究functools模块wraps装饰器的用途
《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节的一个例子(有些许改动):
# coding: utf-8
# Filename: decorator_wraps_test.py
# 2014-07-05 18:58
import sys debug_log = sys.stderr def trace(func):
if debug_log:
def callf(*args, **kwargs):
"""A wrapper function."""
debug_log.write('Calling function: {}\n'.format(func.__name__))
res = func(*args, **kwargs)
debug_log.write('Return value: {}\n'.format(res))
return res
return callf
else:
return func @trace
def square(x):
"""Calculate the square of the given number."""
return x * x if __name__ == '__main__':
print(square(3))
输出:
Calling function: square
Return value: 9
9
这个例子中,我们定义了一个装饰器trace,用于追踪函数的调用过程及函数调用的返回值。如果不用装饰器语法,我们也可以这样写:
def _square(x):
return x * x square = trace(_square)
上面两段代码,使用装饰器语法的版本和不用装饰器语法的版本实际上是等效的。只是当我们使用装饰器时,我们不必再手动调用装饰器函数。
嗯。trace装饰器看起来棒极了!假设我们把如上代码提供给其他程序员使用,他可能会想看一下square函数的帮助文档:
>>> from decorator_wraps_test import square
>>> help(square) # print(square.__doc__)
Help on function callf in module decorator_wraps_test: callf(*args, **kwargs)
A wrapper function.
看到这样的结果,使用decorator_wraps_test.py模块的程序员一定会感到困惑。他可能会带着疑问敲入如下代码:
>>> print(square.__name__)
callf
这下,他可能会想看一看decorator_wraps_test.py的源码,找一找问题究竟出现在了哪里。我们知道,Python中所有对象都是“第 一类”的。比如,函数(对象),我们可以把它当作普通的数据对待:我们可以把它存储到容器中,或者作为另一个函数的返回值。上面的程序中,在 debug_log为真的情况下,trace会返回一个函数对象callf。这个函数对象就是一个“闭包”,因为当我们通过:
def _square(x): return x * x
square = trace(_square)
把trace返回的callf存储到square时,我们得到的不仅仅是callf函数执行语句,还有其上下文环境:
>>> print('debug_log' in square.__globals__)
True
>>> print('sys' in square.__globals__)
True
因此,使用装饰器修饰过的函数square,实际上是一个trace函数返回的“闭包”对象callf,这就揭示了上面help(square)以及print(square.__name__)的输出结果了。
那么,怎样才能在使用装饰器的基础上,还能让help(square)及print(square.__name__)得到我们期待的结果呢?这就是functools模块的wraps装饰器的作用了。
让我们先看一看效果:
# coding: utf-8
# Filename: decorator_wraps_test.py
# 2014-07-05 18:58
import functools
import sys debug_log = sys.stderr def trace(func):
if debug_log:
@functools.wraps(func)
def callf(*args, **kwargs):
"""A wrapper function."""
debug_log.write('Calling function: {}\n'.format(func.__name__))
res = func(*args, **kwargs)
debug_log.write('Return value: {}\n'.format(res))
return res
return callf
else:
return func @trace
def square(x):
"""Calculate the square of the given number."""
return x * x if __name__ == '__main__':
print(square(3))
print(square.__doc__)
print(square.__name__)
输出:
Calling function: square
Return value: 9
9
Calculate the square of the given number.
square
很完美!哈哈。这里,我们使用了一个带参数的wraps装饰器“装饰”了嵌套函数callf,得到了预期的效果。那么,wraps的原理是什么呢?
首先,简要介绍一下带参数的装饰器:
>>> def trace(log_level):
def impl_f(func):
print(log_level, 'Implementing function: "{}"'.format(func.__name__))
return func
return impl_f >>> @trace('[INFO]')
def print_msg(msg): print(msg) [INFO] Implementing function: "print_msg"
>>> @trace('[DEBUG]')
def assert_(expr): assert expr [DEBUG] Implementing function: "assert_"
>>> print_msg('Hello, world!')
Hello, world!
这段代码定义了一个带参数的trace装饰器函数。因此:
@trace('[INFO]')
def print_msg(msg): print(msg)
等价于:
temp = trace('[INFO]')
def _print_msg(msg): print(msg)
print_msg = temp(_print_msg)
相信这样类比一下,带参数的装饰器就很好理解了。(当然,这个例子举得并不好。《Python参考手册》上有一个关于带参数的装饰器的更好的例子,感兴趣的童鞋可以自己看看 。)
接下来,让我们看看wraps这个装饰器的代码吧!
让我们先找到functools模块文件的路径:
>>> import functools
>>> functools.__file__
'D:\\Program Files\\Python34\\lib\\functools.py'
下面,把wraps相关的代码摘录出来:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
从代码中可以看到,wraps是通过functools模块中另外两个函数:partial和update_wrapper来实现其功能的。让我们看一看这两个函数:
1. partial函数
partial函数实现对函数参数进行部分求值(《Python参考手册》中4.9有这么一句:函数参数的部分求值与叫做柯里化(currying)的过程关系十分密切。虽然不是太明白,但感觉很厉害的样子!2014-07-07 15:05追加内容:在百度博客中,zotin大哥回复了我,并对函数式编程中柯里化概念做了一些解释。):
>>> from functools import partial
>>> def foo(x, y, z):
print(locals())
>>> foo(1, 2, 3)
{'z': 3, 'y': 2, 'x': 1}
>>> foo_without_z = partial(foo, z = 100)
>>> foo_without_z
functools.partial(<function foo at 0x00000000033FC6A8>, z=100)
>>> foo_without_z is foo
False
>>> foo_without_z(10, 20)
{'z': 100, 'y': 20, 'x': 10}
这里,我们通过partial为foo提供参数z的值,得到了一个新的“函数对象”(这里之所以加个引号是因为foo_without_z和一般的函数对象有些差别。比如,foo_without_z没有__name__属性。)foo_without_z。因此,本例中:
foo_without_z(10, 20)
等价于:
foo(10, 20, z = 100)
(比较有趣的一点是,foo_without_z没有__name__属性,而其文档字符串__doc__也和partial的文档字符串很相像。此外, 我认为,这里的partial和C++标准库中的bind1st、bind2nd这些parameter binders有异曲同工之妙。这里没有把partial函数的实现代码摘录出来,有兴趣的童鞋可以自己研究一下它的工作原理。)
因此,wraps函数中:
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
实际上是返回一个对update_wrapper进行部分求值的“函数对象”。因此,上例中使用了wraps装饰器的decorator_wraps_test.py的等价版本如下:
def trace(func):
if debug_log:
def _callf(*args, **kwargs):
"""A wrapper function."""
debug_log.write('Calling function: {}\n'.format(func.__name__))
res = func(*args, **kwargs)
debug_log.write('Return value: {}\n'.format(res))
return res _temp = functools.wraps(func)
callf = _temp(_callf)
return callf
else:
return func
对wraps也进行展开:
def trace(func):
if debug_log:
def _callf(*args, **kwargs):
"""A wrapper function."""
debug_log.write('Calling function: {}\n'.format(func.__name__))
res = func(*args, **kwargs)
debug_log.write('Return value: {}\n'.format(res))
return res _temp = functools.partial(functools.update_wrapper,
wrapped = func,
assigned = functools.WRAPPER_ASSIGNMENTS,
updated = functools.WRAPPER_UPDATES)
callf = _temp(_callf)
return callf
else:
return func
最后,对partial的调用也进行展开:
def trace(func):
if debug_log:
def _callf(*args, **kwargs):
"""A wrapper function."""
debug_log.write('Calling function: {}\n'.format(func.__name__))
res = func(*args, **kwargs)
debug_log.write('Return value: {}\n'.format(res))
return res callf = functools.update_wrapper(_callf,
wrapped = func,
assigned = functools.WRAPPER_ASSIGNMENTS,
updated = functools.WRAPPER_UPDATES) return callf
else:
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 。有兴趣的童鞋可以去阅读以下。
探究functools模块wraps装饰器的用途的更多相关文章
- 关于functools模块的wraps装饰器用途
测试环境:Python3.6.2 + win10 + Pycharm2017.3 装饰器之functools模块的wraps的用途: 首先我们先写一个装饰器 # 探索functools模块wraps ...
- python中functools.wraps装饰器的作用
functools.wraps装饰器用于显示被包裹的函数的名称 import functools def node(func): #@functools.wraps(func) def wrapped ...
- wraps装饰器的使用
functools模块中的wraps装饰器 说明 使用functools模块提供的wraps装饰器可以避免被装饰的函数的特殊属性被更改,如函数名称__name__被更改.如果不使用该装饰器,则会导致函 ...
- python学习笔记2-functools.wraps 装饰器
wraps其实没有实际的大用处, 就是用来解决装饰器导致的原函数名指向的函数 的属性发生变化的问题: 装饰器装饰过函数func, 此时func不是指向真正的func,而是指向装饰器中的装饰过的函数 i ...
- wraps装饰器的作用
装饰器的本质是一个闭包函数,作用在于不改变原函数功能和调用方法的基础上给它添加额外的功能.装饰器在装饰一个函数时,原函数就成了一个新的函数,也就是说其属性会发生变化,所以为了不改变原函数的属性,我们会 ...
- python functools.wraps装饰器模块
# -*-coding=utf-8 -*-#实现一个函数执行后计算执行时间的功能 __author__ = 'piay' import time, functools def foo(): ''' 定 ...
- python wraps装饰器
这是一个很有用的装饰器.看过前一篇反射的朋友应该知道,函数是有几个特殊属性比如函数名,在被装饰后,上例中的函数名foo会变成包装函数的名字 wrapper,如果你希望使用反射,可能会导致意外的结果.这 ...
- python函数与模块(装饰器,文件处理,迭代器等)
os模块 os.system('命令') 利用python调用系统命令,命令可以是以列表或者元组内的元素形式* res import os res=os.system('ipconfig') prin ...
- python的wrapt模块实现装饰器
wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器.使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsou ...
随机推荐
- GCC的符号可见性——解决多个库同名符号冲突问题
引用自:https://github.com/wwbmmm/blog/wiki/gcc_visibility 问题 最近项目遇到一些问题,场景如下 主程序依赖了两个库libA的funcA函数和libB ...
- Python 的 six模块简介
Python 的 six模块简介 six : Six is a Python 2 and 3 compatibility library Six没有托管在Github上,而是托管在了Bitbucket ...
- ES系列十四、ES聚合分析(聚合分析简介、指标聚合、桶聚合)
一.聚合分析简介 1. ES聚合分析是什么? 聚合分析是数据库中重要的功能特性,完成对一个查询的数据集中数据的聚合计算,如:找出某字段(或计算表达式的结果)的最大值.最小值,计算和.平均值等.ES作为 ...
- oracle 远程导入导出(本地win)
导出 exp hongtastock_account/hongtastock_account@192.168.1.22/orcl file=D:\hongta\hongtastock_account. ...
- git免密配置
1.在git安装目录下双击bash.exeC:\DevTools\Git\bin 2.在弹出窗口内输入,回车,回车ssh-keygen -t rsa -C "542113457@qq.com ...
- 高级 Java 面试通关知识点整理!
1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式:动态代理和静态代理,什么时候使用动态代理. ...
- 018_mac写权限的问题
一. 有时候运行一些软件的时候,操作系统下的文件没有写权限.我在使用"HostAdmin App"时碰到了这个问题,google的链接供参考:https://code.google ...
- centos系统设置通过windows代理上网
网络环境说明: 物理机windows xp sp3系统 ip:192.168.29.21(通过路由上网,有权限设置proxy给其他机器代理上网) 虚拟机centos5.5系统 ip:192.168.2 ...
- 通达OA批量处理没有结束但前台显示已经结束的流程
问题描述: 通达OA系统出现大量流程没有结束,系统显示结束的问题 通过查询操作系统日志,数据库日志,包括程序日志没有发现异常,通过观察发现大量的流程结束时间都是在2016-02-16 17:32:XX ...
- LinkedList源码分析笔记(jdk1.8)
1.特点 LinkedList的底层实现是由一个双向链表实现的,可以从两端作为头节点遍历链表. 允许元素为null 线程不安全 增删相对ArrayList快,改查相对ArrayList慢(curd都会 ...