目录
背景
第一部分 准备知识
第二部分
Python
中的装饰器第三部分 最佳实践
第四部分 高阶使用
第五部分 Python内置装饰器
参考文献及资料
背景
第一部分 准备知识
1.1 Python中一切皆对象
Python语言中一切皆对象,变量都是对象的引用,包括函数。这有点像C语言的指针。我们可以先创建一个对象,并给对象一定的值,这叫做实例化。先看一下下面的案例:
1 | import sys |
对于上面代码进行一下说明:
- 变量名
show_input
、object_func
、object_func_other
都是指向同一个函数对象(共享引用),并且增加括号后均能调用。 - 可以使用
sys.getrefcount
方法查看对象被引用的次数,这里除了3个变量引用:show_input
、object_func
、object_func_other
,还有方法的临时引用。所以计数是4。当使用del
删除一个变量引用(object_func
)后,对象引用计数变为3。这时候调用object_func()
就会报错:NameError: name 'object_func' is not defined
。 - 一个对象如果引用次数为:0,
Python
会启动垃圾回收机制,回收对象,释放资源。
1.2 局部函数
Python
支持在函数体内定义函数,这种在函数体内被定义的函数称为局部函数。
在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效。例如:
1 | def show_input(input_context="default"): |
对于上面代码进行一下说明:
- 函数
show_input
中我们定义了局部函数print_input
,并进行了内部调用。 print_input
在不能直接调用,否则会报未定义错误:NameError: name 'print_input' is not defined
。
但是函数可以将局部函数作为一个对象进行返回,以便程序在其他作用域中使用局部函数。
1.3 函数返回函数对象
直接看例子:
1 | def show_input(input_context="default"): |
对于上面代码进行一下说明:
- 例子中我们在
show_input
函数中定义了print_input
函数,并且局部函数print_input
引用了外部函数的参数input_context
,当show_input
函数返回print_input
函数的时候,相关的变量保存在返回的函数中,这种称谓闭包(Closure
)。
1.4 函数作为参数
既然函数是对象,所以也可以和其他 Python 对象一样,作为参数传递到另一个函数中去。
1 | def show_input(input_func, print_str: str): |
对于上面代码进行一下说明:
- 函数
print_input
作为参数传给show_input
函数,并在内部进行了调用。
第二部分 Python
中的装饰器
2.1 案例
直接看案例:
1 | def decorator_test(func): |
例子中我们定义了函数decorator_test
,其内部又定义了局部函数decorator_func
,并且将函数作为返回对象。所以@decorator_test
装饰器的本质是将被装饰的函数作为对象参数传入装饰函数中,然后调用执行。
但是当我们查看show_input
的函数名属性show_input.__name__
的时候发现输出是装饰函数名:decorator_func
。这容易造成困惑,所以Python
提供了functools.wraps
方法解决这个问题,如下:
1 | from functools import wraps |
@wraps
接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。
2.2 标准实践
事实上,装饰器可以在环境调用前后分别添加额外的代码逻辑:
1 | from functools import wraps |
第三部分 最佳实践
在日常编码中函数装饰器用途主要有:
权限管理
在函数运行前,提前校验用户数时候有执行权限;
日志管理
函数执行前后补充打印响应的日志信息;
函数执行监控
输出相关函数执行信息;
3.1 权限管理
直接看案例:
1 | from functools import wraps |
3.2 日志管理
看案例:
1 | from functools import wraps |
3.3 函数执行监控
例如我们监控统计函数执行的耗时:
1 | from functools import wraps |
第四部分 高阶使用
4.1 带参装饰器
前面的例子中装饰器都是没有传参的,那么如何实现传参呢?例如日志应用场景,我们将日志内容,输出到指定的文件中。
1 | from functools import wraps |
看这个原理其实就是套娃,等价于:
1 | logger(log_file_path='file.log')(show_input) |
4.2 装饰器类
前面例子里的装饰器都是函数,其实装饰器语法其实并不要求本身是函数,而只要是一个可调用对象即可。我们将上面日志装饰器改造成类装饰器:
1 | from functools import wraps |
实现效果是相同的。
4.3 叠加装饰器
装饰器多个能否一同使用,即叠加使用。是可以的,例如同时使用日志装饰器和函数执行监控装饰器:
1 | from functools import wraps |
实际执行逻辑如下(套娃):
1 | logger(log_file_path='file.log')(decorator_time(show_input)) |
其中__call__()
的作用是使实例能够像函数一样被调用。
4.4 装饰器和闭包
通常函数体重变量是局部变量,函数调用执行完毕后,该变量将被回收。但是对于局部函数(前文”函数返回函数对象”)情况有点不同了,即闭包(Closure
)。
1 | from functools import wraps |
例子中count变量称为自由变量,又称为外层变量被闭包捕获。这样看装饰器天然就是一个闭包。
既然装饰器就是闭包,那么其中的自由变量就不会随着原函数的返回而销毁,而是伴随着原函数一直存在。利用这一点,装饰器就可以携带状态。
我们看下面的案例:
1 | from functools import wraps |
例子中我们在局部函数中声明count
变量不是一个局部变量。
4.5 小结
- 装饰器是闭包的一种应用,是返回值为函数的高阶函数;
- 装饰器修饰可调用对象,也可以带有参数和返回值;
- 装饰器中可以保持状态。
第五部分 Python内置装饰器
Python
中内置装饰器有三个:@property
、@staticmethod
、@classmethod
。我们尽量在一个案例中体现并说明:
1 | class Person: |
5.1 特性装饰器@property
例子中info
函数使用了@property
注释,这样调用类中的info
方法,像引用类中的字段属性一样。注意下面调用时候没有括号。
1 |
|
5.2 静态方法装饰器@staticmethod
将类中的方法装饰为静态方法,即类不需要创建实例的情况下,可以通过类名直接引用。
1 |
|
案例中,在类没有实例化前就可以直接调用:Person.sleep()
,当然实例化后也可以调用:Tom.sleep()
。
5.3 类方法装饰器@classmethod
1 | test_name = "jack" |
方法调用的时候无需实例化类。
参考文献及资料
1、PEP 318 – Decorators for Functions and Methods,链接:https://peps.python.org/pep-0318/