python装饰器_python装饰器完全指南之一
设我们有一组函数,它们有共同的错误处理方法,比如打印日志和记录审计信息等。很显然,在每一个函数中都重复这些逻辑是不恰当的,它们应该被提炼到一个函数里,在这个函数的保护下,再调用我们的业务逻辑处理功能。
尽管错误处理可能占据代码的主要部分,但业务逻辑才是程序的核心价值。因此,从代码结构上看,错误处理应该处于可被忽略的非中心地带。如果我们每次调用业务逻辑处理功能前,都要先显式地从一个错误处理函数开始,这种写法显然是头重脚轻,也会打断代码阅读者的思绪。基于这些原因,开发语言引入了面向切面的编程(AOP):把与主业务无关的事情,放到代码之外去做。
装饰器是AOP编程中不可缺少的语法糖。通过装饰器语法,可以使得程序更简洁易读。本文对装饰器的基础原理、一般写法、corner case和常见场景进行了探讨。
三个系列共三篇文章,可能是互联网上最全、最深入的python装饰器指南之一。
从一个最简单的装饰器开始
假设我们有一个功能函数(从现在开始,我们把被装饰器修饰,完成业务逻辑的那些函数称作功能函数,以区别于装饰器函数),出于调试目的,我们希望打印出它的参数及每次调用的返回值。
假设功能函数如下:
# block 1def buggy_incr_by(number): import random return random.randint(0,10) + number我们可以定义这样一个函数:
# block 2def snoop(func): def wrapper(number): print(f" >>> invoke {func.__name__} with parameter: {number}") result = func(number) return result print(f"<<< {func.__name__} returned {result}") return wrapper现在,运用装饰器语法:
# block 3@snoopdef buggy_incr_by(number): import random return random.randint(0,10) + number# call and check the resultbuggy_incr_by(3)# --- output --->>> invoke buggy_incr_by with parameter: 3<<< buggy_incr_by returned 13装饰器究竟是如何工作的?
现在我们来看一看这一切是如何发生的。
这里最基本的原理有:
1. 在python中,function(函数)也是一种对象(当不带括号引用时)。你可以任意选择一个函数f,通过dir(f)来查看它有哪些属性。
2. 在函数内也可以定义函数,并返回这个定义的函数对象。这是因为根据原理1,函数本身也是对象。
3. 模块加载器调用exec_module时,会查找和解析@语句,通过执行 func = decorator(func),重新定义功能函数。
在上面的例子中,我们定义了装饰器函数snoop,它接受一个规定好的参数(必须),即功能函数对象本身。decorator的主要功能是定义并返回一个函数对象(下面称之为替换函数)。这个函数对象中,完成我们需要的面向切面的功能,并且调用功能函数,返回其返回值。
当上述代码所在的模块文件被importlib加载并执行时,加载器(Loader)发现存在'@'语法糖,于是执行:
# block 4buggy_incr_by = snoop(buggy_incr_by)结合snoop的代码不难发现,snoop将返回一个名为wrapper的函数对象(替换函数),赋值给buggy_incr_by,所以此后调用buggy_incr_by,实际上就是在调用这个wrapper。
下面是写一个最简单的装饰器时的一般要诀:
1. 装饰器decorator只接受一个形参(名字可以任意取),这个形参将模块加载器调用exec_module时,从@注解的下一行函数的定义中找到被定义的函数对象传入。见上一个代码块的说明。
2. 装饰器的函数体必须定义并返回一个wrapper函数(名字可以任意取)。这个wrapper(替换)函数的签名一般情况下等同于功能函数。例外情况在下文中叙述。
3. 在添加装饰器注解(即'@'语法)时,不需要显式地将功能函数参数传给装饰器,这将由模块加载器自动完成。因此,如果装饰器只有这一个参数,注解中必须是不带括号引用,见上面第2行。
4. 如果功能函数有返回值,则在wrapper的函数体中,也需要将返回值返回,参见block 2第6行。
通过上述分析,我们还有几个重要的结论:
1. 装饰器语法在模块加载时就运行了,并且重新定义了功能函数的指向(即上述代码中的wrapper)。
2. 在定义wrapper时,功能函数并没有真正被调用,因此需要延迟绑定的参数,比如self对象,此时是不存在的。
3. 在代码的其它地方调用功能函数时,实际上是在调用上述wrapper,此时实现参数的绑定(即给形参赋值)。
找回丢失的调试信息
从前面的分析可以看出,功能函数在模块加载过程中,实际上被替换成了wrapper函数。我们可以通过下面的测试来发现这一点:
print(buggy_incr_by.__name__)# --output---wrapper显然buggy_incr_by已经被替换了。但这里也暴露出一个问题:如果程序出错,则在需要显示栈信息的地方,则都会显示为wrapper,而不是功能函数的名字。比如下面一例:
def snoop(func): def wrapper(number): print("passed in param is ", number) result = func(number) print("buggy_incr_by returned ", result) return wrapper@snoopdef buggy_incr_by(number): import random breakpoint() return random.randint(0,10) + numberbuggy_incr_by(3)我们在第11行放置了一个断点,运行之后,我们查看堆栈信息如下:
-> command.run()-> self.more = self.interpreter.runsource(text, '', symbol) /home/.../code.py(74)runsource()-> self.runcode(code) /home/.../code.py(90)runcode()-> exec(code, self.locals) (14)() (4)wrapper()断点设置在bugg_incr_by中,但显示的最底层的函数名却为wrapper,这会使得调试变困难,因此我们需要更正这一信息。
函数作为一种对象,它有以下元属性:
# __module__, __name__, __qualname__, __doc__, __annotations__for name in ['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']: print(getattr(buggy_incr_by, name))# --output--__main__wrappersnoop..wrapperNone{}我们需要用功能函数的这些元属性来改写替换函数的相关属性:
setattr(buggy_incr_by, '__name__', 'gime new name')for name in ['__module__', '__name__', '__qualname__', '__doc__', '__annotations__']: print(getattr(buggy_incr_by, name))#--output--__main__gime new namesnoop..wrapperNone{}通过使用setattr,我们可以很容易替换掉这些信息。我们看到buggy_incr_by现在有了新的名字,即'gime new name'
不过,我们没有必要亲自去做这些琐事。我们可以在代码段block 2的第三行,即在wrapper之前,调用functools.wraps来为我们解决这个问题,这里functools.wrapper是另一个装饰器:
import functoolsdef snoop(func): @functools.wraps(func) # wraps需要接收func参数 def wrapper(number): print("passed in param is ", number) result = func(number) print("buggy_incr_by returned ", result) return wrapper@snoopdef buggy_incr_by(number): import random breakpoint() return random.randint(0,10) + numberbuggy_incr_by.__name__注意第3行的注释。很显然functools.wraps需要这个参数,因为它要从func中获取`__name__`,` __qualname__`, `__doc__`等信息,以便去更新下面的wraper。实际上,functools.wraps是接收了两个函数对象作为参数。
总结
以上是生活随笔为你收集整理的python装饰器_python装饰器完全指南之一的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: mysql 2003报错_为什么不建议在
- 下一篇: python怎么选择安装位置图片_怎么下