参考:https://zhuanlan.zhihu.com/p/45458873
假设现在有一个 add
求和函数,想要统计他的运行时长,最简单的方法可以这样:
import time
def add(a, b):
start = time.time()
print(a + b)
# 模拟耗时操作
time.sleep(0.1)
long = time.time() - start
print('共耗时{}秒'.format(long))
add(1, 1)
这样做可以实现需求,但是对原函数做了修改,不仅增加了耦合性,扩展和复用也变得难以实现。
假如我想对其他函数也进行运行时长统计,就需要一个不改变原来函数而且可以复用的新函数来实现:
import time
def timer(func, *args):
start = time.time()
func(*args)
# 模拟耗时操作
time.sleep(0.1)
long = time.time() - start
print('共耗时{}秒'.format(long))
def add(a, b):
print(a + b)
timer(add, 1, 1)
这样没有改变原函数,但是改变了函数调用方式,每个调用 add
的地方都需要修改,这么做只是转嫁了矛盾而已。
既不能修改原函数,又不能改变调用方式,那该怎么办呢?装饰器是时候登场了。
在写装饰器之前先了解两个概念:高阶函数和闭包
高阶函数:接受函数为入参,或者把函数作为结果返回的函数。后者称之为嵌套函数。
闭包:指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。概念比较晦涩,简单来说就是嵌套函数引用了外层函数的变量。
嵌套函数和闭包可以理解为是同时存在的,上面的 timer
已经是高阶函数了,它接受函数作为入参,我们把它改造为嵌套函数实现装饰器:
import time
def timer(func, *args):
def wrapper(*args, **kwargs):
start = time.time()
# 此处拿到了被装饰的函数 func
func(*args, **kwargs)
# 模拟耗时操作
time.sleep(0.1)
long = time.time() - start
print('共耗时{}秒'.format(long))
# 返回内层函数的引用
return wrapper
@timer
def add(a, b):
print(a + b)
add(1, 2)
timer
被我们改造成了装饰器,它接受被装饰函数为入参,返回内部嵌套函数的引用(注意:此处并未执行函数),内部嵌套函数 wrapper
持有被装饰函数的引用即 func
。
@
是 Python的 语法糖,它的作用类似于:
# 此处返回的是 timer.<locals>.wrapper 函数引用
add = timer(add)
add(1, 2)
装饰器的加载到执行的流程:
- 模块加载
- 遇到
@
,执行timer函数,传入add
函数
- 遇到
- 生成
timer.<locals>.wrapper
函数并命名为add
,其实是覆盖了原同名函数
- 生成
- 调用
add(1, 2)
- 调用
- 去执行
timer.<locals>.wrapper(1, 2)
- 去执行
-
wrapper
内部持有原add
函数引用(func)
,调用func(1, 2)
-
- 继续执行完
wrapper
函数
- 继续执行完
如果存在多个装饰器,执行顺序是什么样的呢?
def test1(func):
def wrapper(*args, **kwargs):
print('before test1 ...')
func(*args, **kwargs)
print('after test1 ...')
return wrapper
def test2(func):
def wrapper(*args, **kwargs):
print('before test2 ...')
func(*args, **kwargs)
print('after test2 ...')
return wrapper
@test2
@test1
def add(a, b):
print(a + b)
add(1, 2)
输出结果:
before test2 ...
before test1 ...
3
after test1 ...
after test2 ...
如果把 add
函数比喻为圆心,test1
为近心端,test2
为远心端,那么执行的过程就好比一颗子弹从远心端沿着直径的轨迹穿过圆心再从远心端穿出。
再形象一点,可以把装饰器想象成洋葱,由近及远对函数进行层层包裹,执行的时候就是拿一把刀从一侧开始切,直到切到另一侧结束。
理解了装饰器之后,我们可以思考一下,带参数的装饰器该怎么写呢?
我们知道装饰器最终返回的是嵌套函数的引用,只要记住这点,装饰器就任由我们发挥了。写一个带参数的装饰器:
import time
def timer(second): # 传入装饰器的参数
def _timer(func): # 传入要装饰的函数
def wrapper(*args, **kwargs): # 被装饰的函数的参数
start = time.time()
func(*args, **kwargs)
# 模拟耗时操作
time.sleep(second)
long = (time.time() - start)
print('共耗时{}秒'.format(long))
return wrapper
return _timer
@timer(0.1)
def add(a, b):
print(a + b)
add(1, 2)
上例演示了一个可以用参数设定暂停时间的装饰器。
可能会有人有疑问,经过装饰器之后的函数还是原来的函数吗?原来的函数肯定还存在的,只不过真正调用的是装饰后生成的新函数。
那岂不是打破了“不能修改原函数”的规则?
是的,看下面的示例:
>>> print(add)
<function timer.<locals>._timer.<locals>.wrapper at 0x02EDAA50>
>>> print(add.__name__)
wrapper
>>> print(add.__doc__)
None
为了消除装饰器对原函数的影响,我们需要伪装成原函数,拥有原函数的属性,我们使用 Python 的 functools
模块来实现。
import time, functools
def timer(second):
def _timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
# 此处拿到了被装饰的函数 func
func(*args, **kwargs)
# 模拟耗时操作
time.sleep(second)
long = (time.time() - start)
print('共耗时{}秒'.format(long))
return wrapper
return _timer
@timer(0.1)
def add(a, b):
print(a + b)
add(1, 2)
>>> print(add)
<function add at 0x02EFAA50>
>>> print(add.__name__)
add
>>> print(add.__doc__)
None
functools.wraps
对我们的装饰器函数进行了装饰之后,add
表面上看起来还是 add
。
网友评论