虽然Python的装饰器是一个强大且有用的功能,但如果使用不当,也并非无懈可击。在这个部分,我们将讨论在设计和实现高效装饰器时需要避免的一些常见问题。
被包装对象的数据信息丢失 在将装饰器应用到函数时最常见的问题之一就是,原始函数的一些属性或特性并未被保留下来,从而导致一些不易被追踪的不良副作用。
为了说明这一点,对比这两个装饰器,他们的任务是在函数即将运行时进行日志记录:
1 2 3 4 5 6 7 8 9 10 11 12 def trace (function ): def wrapped (*args, **kwargs ): logger.info("running %s" , function.__qualname__) return function(*args, **kwargs) return wrapped @trace def fun (index: str ): """Processing index.""" logger.info("processing index %s" , index) ...
在语法上是没有问题的,并且可以执行,但是这种用法会把函数fun的信息修改掉
1 2 3 4 5 6 7 8 9 >>> help (fun)Help on function wrapped in module __main__: wrapped(*args, **kwargs) >>> fun.__qualname__'trace.<locals>.wrapped' >>> process_account.__annotations__{}
如果我们将类似的装饰器应用到多个函数上,虽然这些函数本身的名称各不相同,但最终它们都会被称为 “wrapped”,这是一个严重的问题(例如,如果我们希望对函数进行日志记录或跟踪,这将使得调试更加困难)。
另一个问题是,如果我们在这些函数上添加了带有测试的文档字符串,它们会被装饰器的文档字符串所覆盖。因此,当我们使用doctest模块调用代码时,我们想要运行的带有测试的文档字符串将不会执行
对此问题的修复十分简单,只需要使用functools.wraps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from functools import wrapsdef trace (function ): @wraps(function ) def wrapped (*args, **kwargs ): logger.info("running %s" , function.__qualname__) return function(*args, **kwargs) return wrapped @trace def fun (index: str ): """Processing index.""" logger.info("processing index %s" , index)
这样上面的问题就得到了修复
1 2 3 4 5 6 7 8 9 10 11 >>> help (fun)Help on function fun in module __main__: fun(index: str ) Processing index. >>> fun.__qualname__'fun' >>> fun.__annotations__{'index' : <class 'str' >}
最佳实践 在创建装饰器时,应始终使用functools.wraps应用于被包装的函数,如上面正确代码所示
装饰器作用范围错误 引发这个问题的原因,是没有理解装饰器内函数的作用范围,导致一些意料之外的执行错误。
假设我们有一个装饰器,它的目标是在函数开始运行时进行日志记录,然后记录其运行时间:
在python文件decorator_scope.py中定义如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import logging as loggerimport timefrom functools import wrapslogger.basicConfig(level=logger.INFO) def traced_function_execution_time (fun ): logger.info("function %s started" , fun) start_time = time.time() @wraps(fun ) def wrapped (*args, **kwargs ): result = fun(*args, **kwargs) logger.info( "function %s took %.2fs to finish" , fun, time.time() - start_time ) return result return wrapped @traced_function_execution_time def test (delay=0 ): time.sleep(delay)
这时如果我们import这个文件会得到下面的结果
1 2 3 4 5 6 7 8 >>> import decorator_scopeINFO:root:function <function test at 0x7f98f9ec6320 > started >>> test.test()INFO:root:function <function test at 0x7fd98806e320 > took 2.17 s to finish >>> test.test()INFO:root:function <function test at 0x7fd98806e320 > took 4.86 s to finish >>> test.test()INFO:root:function <function test at 0x7fd98806e320 > took 6.18 s to finish
在没有调用函数test的情况下,一部分装饰器的代码被执行了。而且装饰器中的start_time变成了全局变量,无法得到正确的函数执行时间。
修复这个问题依然很简单,只需要修改装饰器traced_function_execution_time
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def traced_function_execution_time (fun ): @wraps(fun ) logger.info("function %s started" , fun) start_time = time.time() def wrapped (*args, **kwargs ): result = fun(*args, **kwargs) logger.info( "function %s took %.2fs to finish" , fun, time.time() - start_time ) return result return wrapped @traced_function_execution_time def test (delay=0 ): time.sleep(delay)
装饰器语法在python运行时始终都会被这样执行:
1 2 3 4 def test (delay=0 ): time.sleep(delay) test=traced_function_execution_time(test)
所以在引入模块时,装饰器中负责接收函数的部分就会被执行,从而导致意想不到的bug。