注:作者编程小白,高手勿喷,如有疏漏,还请指正!
正好在廖大大后面的章节里再次看到装饰器,借此机会再次复习了一遍装饰器,发现又有一些新的理解和收获。
为函数定义添加装饰器时,就会执行装饰器代码
譬如下面这段代码,在装饰器中加上一个 print,输出结果发现 func() 并未被执行,但是已经有了 deco(func) 中的 print,
# Code 1
def deco(func):
print("check where decorator works")
def wrapper(*args, **kw):
print('calling %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@deco
def func():
print("this is defined in func()")
# Result 1
check where decorator works
在上面的代码下面加入 func(),输出结果中就包含了 func() 内部的代码。
# Result 1 with func() in __main__
check where decorator works
calling func():
this is defined in func()
据此分析:
在为函数定义添加装饰器时,相当于就执行了装饰器的代码,其结果相当于定义了 func(*args, **kw) = wrapper(*args, **kw),即使加上 @functools.wraps(func) ,但其代码路径已经从直接 func() 内部跳到了 wrapper() 里面。也就是说,之后在 main 里面调用 func() 时,本质是在调用 wrapper()。
—— 这也是为什么 main 中执行 func() 时不会再次出现 deco 中的 print!
Wrapper() 中的 func(*args, **kw) 是确保 func() 被实际执行的关键所在!
运行下面一段代码,实际结果发现 func() 定义时的内部代码并没有被执行,而 wrapper() 内部的 print 确实已经输出(在添加装饰器时,wrapper() 内部的 print 是不会输出的,参考 Code 1 的分析),
# Code 2
def deco(func):
print("check where decorator works")
def wrapper(*args, **kw):
print('calling %s():' % func.__name__)
#return func(*args, **kw) wrapper的返回已被注释掉
return wrapper
@deco
def func():
print("this is defined in func()")
func() #这里执行一次 func()
# Result 2
check where decorator works
calling func():
是否一定要 return func(*args, **kw)?
不一定!譬如这段代码,其输出并未报错,但结果不包含 func() 中的 print,并且 print(func()) 的结果是 None!
# Code 3 without returning func() but func() returns 1
def deco(func):
print("check where decorator works")
def wrapper(*args, **kw):
print('calling %s():' % func.__name__)
#return func(*args, **kw) wrapper的返回已被注释掉
return wrapper
@deco
def func():
print("this is defined in func()")
return 1 #为func()添加返回值
print(func())
# Result 3 without returning func()
check where decorator works
calling func():
None
而 wrapper() 内部 return func() 的结果如下, 这里的 “this is defined in func()” 和 1 都是由于执行 print(func()) 而依次输出的!
# Result 3 with returning func()
check where decorator works
calling func():
this is defined in func()
1
习题1分析
请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间
贴上代码如下,
# Code 4.1 - Test 1
import time, functools
def metric(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
start_time = time.time()
x = fn(*args, **kw) #在这里获取fn()的返回值,并通过上下的time.time()获取执行时间
end_time = time.time()
print('%s executed in %s ms' % (fn.__name__, 1000 * (end_time - start_time)))
return x #把fn()的返回值作为wrapper()函数的返回值传递给调用"fn()"的代码
return wrapper
# 测试
@metric
def fast(x, y):
time.sleep(0.0012)
return x + y
@metric
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z
f = fast(11, 22)
print(f) #自行添加代码以获取 f 的值
s = slow(11, 22, 33)
print(s) #自行添加代码以获取 s 的值
if f != 33:
print('测试失败!')
elif s != 7986:
print('测试失败!')
这里可以看到 wrapper() 内部未必要 return fn(),而可以在 wrapper() 内先通过参数获取 fn() 的返回值,并把该参数的值作为 wrapper() 的返回值传递给调用“fn()”的代码!
——为什么一定要这么做?
——因为我看到评论区有一个答案在 wrapper() 内部是这么写的,
# Code 4.2.1 - Another Wrapper()
def wrapper(*args, **kw):
start_time = time.time()
fn(*args, **kw)
end_time = time.time()
print('%s executed in %s ms' % (fn.__name__, 1000 * (end_time - start_time)))
return fn(*args, **kw)
return wrapper
其运行结果跟上面的 Code 4.1 一致,一开始我认为也是对的,但是后来仔细一想,我认为其中有问题!譬如这样修改 fast() 和 slow() 函数,
# Code 4.2.2 - Another fast() & slow()
const_fast = 1 #定义全局变量 const_fast 查看其值的变化
const_slow = 1 #定义全局变量 const_slow 查看其值的变化
@metric
def fast(x, y):
global const_fast #声明全局变量 const_fast
time.sleep(0.0012)
print("fast:%s" % const_fast) #打印变量 const_fast
const_fast = const_fast + 1 #[ 重要 ]每当 fast() 被调用一次,全局变量 const_fast 就+1,从而可以通过打印输出查看 fn() 被调用的顺序
return x + y
# const_slow 同理
@metric
def slow(x, y, z):
global const_slow
time.sleep(0.1234)
print("slow:%s" % const_slow)
const_slow = const_slow + 1
return x * y * z
其输出结果如下,可以看到 fast:1 和 fast:2 两次对 fast() 的执行,其中,习题要求的“该函数的执行时间”其实是 fast:1 的运行时间,而 f 的值 33 则是由 fast:2 返回获得,slow:1 和 slow:2 同理。所以,Code 4.2.1 的代码写法其实与习题的要求还是有一些区别的。因此我才写了 Code 4.1 的方法,更加准确!
Result 4.2.2
fast:1
fast executed in 7.000207901000977 ms
fast:2
33
slow:1
slow executed in 126.0073184967041 ms
slow:2
7986
习题2 & 3分析
请编写一个decorator,能在函数调用的前后打印出’begin call’和’end call’的日志。
再思考一下能否写出一个@log的decorator,使它既支持@log()又支持@log(‘execute’)
贴上代码如下,源自一条高手的评论,着实让我佩服!
# Code 5 - Test 2 & 3
import time, functools
def log(arg):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print('\r\n%s %s():' % (arg, fn.__name__)) #加入\r\n为了让输出更清晰
print('begin call') #习题2
res = fn(*args, **kwargs)
print('end call') #习题2
return res #习题2
return wrapper
print("check where decorator works")
if isinstance(arg, str):
return decorator
elif hasattr(arg, '__call__'):
return log('')(arg) #[ 此处是重点! ]
else:
pass
@log
def f1():
print("this is f1()")
@log('execute')
def f2():
print("this is f2()")
f1()
f2()
# Result 5
check where decorator works
check where decorator works
check where decorator works
f1(): #等同于 ''(源自 log('')) + ' ' + 'f1()' + ':'
begin call
this is f1()
end call
execute f2():
begin call
this is f2()
end call
其中习题2的“begin call”和“end call”和习题1中的分析完全一致,在此便不再赘述!关键是习题3以及上面代码的对应设计,真是让我心服口服!
首先分析一下3个“check where decorator works”:
@log
def f1():
- 这里 f1() = log(f1),f1 是函数名,进入 log(arg) 函数内部,不用看 def decorator(fn) 的代码,直接看到 print(“check where decorator works”) 代码,在此执行,所以首先有第一个输出“check where decorator works”!
- 然后 log(f1) 的判断结果为 return log(”)(arg),此处递归用得妙啊!
- 于是 f1() = log(f1) = log(”)(f1)!由 log(”) 再次进入 log(arg) 函数内部 于是有了第二个输出“check where decorator works”!
- 然后这里 log(”) 的判断结果为 return decorator,所以 f1() = log(f1) = log(”)(f1) = decorator(f1)!
- 那么 decorator(f1) 又是什么呢?这时就直接调用 def decorator(fn) 的代码。可以看到 decorator(fn) 的代码内容和最简单的装饰器完全一致,即 f1() = log(f1) = log(”)(f1) = decorator(f1) = wrapper()!
综上,在这段代码执行完时,输出了两个“check where decorator works”,并且定义 f1() = wrapper()!
@log('execute')
def f2():
- 参考廖大大的原文可知,这里 f2() = log(‘execute’)(f2),于是其逻辑等同于 f1() 的第3点,并且会输出第三条“check where decorator works”!
- 其结果一样就是 f2() = wrapper()!
注意 wrapper() 函数中,print(‘%s %s():’ % (arg, fn._name_) 中的 arg 是 log(arg) 的参数,而非 wrapper(*arg, **kw) 的参数!对于 f1(),这里的 arg = ”;对于 f2(),这里的 arg = ‘execute’。
后面 f1() 和 f2() 的执行输出就不赘述了。
总而言之,核心代码在于“return log(”)(arg)”,妙不可言啊!