文章目录
- 前言
- 用函数调用函数
- 用函数返回函数
- 用函数返回包装函数
- 使用@
- 再进一步
- 应用场景
前言
这次遇到了一个比较神奇的面试题:给定方法
def add(x, y):return x + y
要求在不改变源代码的前提下,使用装饰器,为add
方法增加运行时间输出的功能。
用函数调用函数
其实本身而言并没有什么特别难的内容,只是单纯的比较综合罢了。
首先,我们尝试一下函数包函数,也就是说,我们将add
函数包装为其他的函数:
import time
def decorate_add(x, y):start = time.time()def add(x, y):return x + yresult = add(x, y)end = time.time()time_consumption = end - startprint(result, time_consumption)
看起来没什么问题,连输出也不需要说明,一目了然。但是呢,这样会不会太简单了?我们先升级一下难度。
用函数返回函数
没错,函数返回函数,然后调用函数,就能够获得结果。就像这样:
import timedef time_consume_f(f):start = time.time()print(f())end = time.time()print(end - start)return time_consume_fdef add():return 1e3decorater = time_consume_f(add)
decorater(add)
输出结果就会是
1000.0
0.0
1000.0
0.0
没错,因为f
本身具有print
功能,在调用decorater
的时候又调用了一遍。
当然,你也看到,这样的方法没办法继续输入参数了。得再想想办法。
用函数返回包装函数
既然我们单纯的用一层函数包装不够,我们使用两层呢?外层先传入函数,内层使用相同的参数,并调用外层传入的函数,是不是就能解决问题呢?
试试看:
import time
def decorater(func):def wrapped(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print('time: ', end - start)return resultreturn wrappeddef add(x, y):return x + ywrapped_function = decorater(add)
wrapped_function(1, 2)
输出:
0.0
3
这下是我们想要的结果了。
使用@
如果你使用Flask
,你会想到你曾经在一些controller
方法上增加一个@app.route('/api')
,让前端从http://localhost:8080/api
中访问到你的方法。
那么,现在我们应该如何使用呢?
其实并不需要做出太多改动:
import time
def decorater(func):def wrapped(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print('time: ', end - start)return resultreturn wrapped@decorater
def add(x, y):return x + yadd(1, 2)
输出还是老样子:
0.0
3
看来@
只是一个简写。
再进一步
其实到此为止已经足够了。但是既然官方都有,那我们就用一下吧。
import time
from functools import wrapsdef time_consume(f):@wraps(f)def wrapTheFunction(*args, **kargs):start = time.time()result = f(*args, **kargs)end = time.time()print(end - start)return resultreturn wrapTheFunction@time_consume
def add(x, y):return x + yprint(add(1, 2))
其实效果是完全相同的:
0.0
3
但是呢,@wraps
的作用主要就是装饰一个函数,并赋值函数名称、注释文档、参数列表等等
接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。总之就是,比我们单纯用双层函数功能全多了。
应用场景
当然,我们现在只考虑了两层嵌套的用法,也就是对一个函数进行包装,从而无侵入地使得这个函数拥有更多的功能。
而如果我再嵌套一层呢?没错,还能继续接收参数。
这也就是Flask
中@app.route()
中能够有那么多参数的真正原因。我们通过这样的三层嵌套实现相对较为统一的、相对比较重复的内容,从而让我们在实现业务的过程中能够更专注于业务,剩下的装饰一下就好了。
我们举个例子吧。比如,我需要将日志保存在指定的文件中,但是我又不希望在代码中植入日志编辑的逻辑,因为这样只会让代码更为复杂。那么该怎么办呢?当然就是使用装饰器,通过截取函数中的所有print
内容,然后再将这些内容写入文件。
我们可以首先定义一个三层嵌套的装饰器:
import sys
from contextlib import redirect_stdout
from io import StringIOdef redirect_print_to_log(log_path="app.log"):def decorator(func):def wrapper(*args, **kwargs):# 创建一个字符串流用于捕获print输出temp_stdout = StringIO()# 保存原始的sys.stdoutoriginal_stdout = sys.stdouttry:# 重定向标准输出到字符串流with redirect_stdout(temp_stdout):result = func(*args, **kwargs)# 将捕获的输出追加到指定的日志文件中with open(log_path, "w", encoding='utf-8') as log_file:log_file.write(temp_stdout.getvalue())finally:# 恢复原始的sys.stdoutsys.stdout = original_stdoutreturn resultreturn wrapperreturn decorator
当然,你也可以使用@wraps
装饰器,这里就不再赘述了。这里实际上就是在双层嵌套的基础上再加一层嵌套,使得装饰器可以接收参数,就像这样:
@redirect_print_to_log("your_file.log")
def your_function(*args, **kwargs):print("your print")
在这段代码中,装饰器接收的参数就是log_path
,即日志文件的路径your_file.log
。这样就能够截取your_function
中所有的print
输出(这里就是截取到了your print
),然后将这些输出写入到your_file.log
中。
最后,打开your_file.log
,文件中就存在your print
字样了。