zoukankan      html  css  js  c++  java
  • 探究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: {}
    '.format(func.__name__))
                res = func(*args, **kwargs)
                debug_log.write('Return value: {}
    '.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: {}
    '.format(func.__name__))
                res = func(*args, **kwargs)
                debug_log.write('Return value: {}
    '.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: {}
    '.format(func.__name__))
                res = func(*args, **kwargs)
                debug_log.write('Return value: {}
    '.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: {}
    '.format(func.__name__))
                res = func(*args, **kwargs)
                debug_log.write('Return value: {}
    '.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: {}
    '.format(func.__name__))
                res = func(*args, **kwargs)
                debug_log.write('Return value: {}
    '.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 。有兴趣的童鞋可以去阅读以下。

  • 相关阅读:
    PHP实现无限极分类
    html2canvas生成并下载图片
    一次线上问题引发的过程回顾和思考,以更换两台服务器结束
    Intellij IDEA启动项目报Command line is too long. Shorten command line for XXXApplication or also for
    mq 消费消息 与发送消息传参问题
    idea 创建不了 java 文件
    Java switch 中如何使用枚举?
    Collections排序
    在idea 设置 git 的用户名
    mongodb添加字段和创建自增主键
  • 原文地址:https://www.cnblogs.com/myd7349/p/how_to_use_wraps_of_functools.html
Copyright © 2011-2022 走看看