函数装饰器(上)
就像我前面所讲的那样,我不止一次在面试中被问到装饰器,这章我会为你揭开它的面纱。
1、装饰器基础知识
不要把装饰器想的那么复杂,装饰器就是一个可调用的对象,只不过其参数是一个函数对象(如果你看过上一章,应该知道函数就是一个对象,可以作为参数进行传递),和我们往函数里面传入列表对象,整数对象并没有什么差别,我们先看一个简单的例子:
# 示例1
def deco(af):
def inner():
print('来自inner函数的输出')
print('deco-inner ID: %d' % id(inner))
return inner
def target(): # 用deco来装饰target
print('来自target函数的输出')
print('未装饰前target ID: %d' % id(target))
target = deco(target)
target()
print('装饰后target ID: %d' % id(target))
# 返回
未装饰前target ID: 2747427204840
deco-inner ID: 2747422507080
来自inner函数的输出
装饰后target ID: 2747422507080
我们首先在deco里面设置了一个嵌套函数inner,其id为080,接下来我们使用使用了target = deco(target)语句,传入原始id为840的target函数对象,但是返回的inner对象,并将inner对象赋值给target,所以target的id必然发生变换,因为target变量所指向的对象都变了。这就是装饰器,传入一个函数,然后返回(替换为)另一个函数。
严格来说,装饰器只是语法糖,可以直接将其视为一个可调用对象,装饰器可以写成下面两种形式:
# 示例2
@deco
def target(): # 用deco来装饰target
print('来自target函数的输出')
def target():
print('来自target函数的输出')
target = deco(target) # 用deco来装饰target
两者是等价的,但是第一种更常用一些。
2、Python何时执行装饰器
尽管这两种形式都属于装饰器的定义,但是我们也可以看出装饰器的一个关键特性:装饰器在被装饰的函数定义之后立即运行。这个不难理解,因为参照示例2的第二端代码,在定义装饰器的时候有一个赋值语句,所以在定义之后立即运行。当然,我们要被装饰的target对象只有在调用的时候才会执行。
3、实现一个简单的函数装饰器
我们以一个例子来看装饰器的实现过程。被装饰函数是一个计算阶乘的函数,现在想不更改原始函数的基础上记录每一次计算所耗费的时间,具体实现过程如下:
import time
# 装饰器最外层输入参数是一个函数
def clock(func): # 1
name = func.__name__
# 如果存在其他内部参数应嵌套一个内部函数进行参数获取
def clocked(*arg):
n = 1
if arg:
n = int(arg[0])
start = time.perf_counter()
# 内部调用被装饰函数获取结果
result = func(n) # 2
cost_time = time.perf_counter() - start
print("[{0:.8f}s] {1:s}({2:d})->{3:d}".format(cost_time, name, n,
result))
return result # 3
# 内部函数返回真实计算结果
# 最外层函数返回函数对象
return clocked
# 开始装饰啦
@clock # 4
def factoral(n):
return 1 if n < 2 else n * factoral(n - 1)
>>>factoral(6)
[0.00000030s] factoral(1)->1
[0.00005200s] factoral(2)->2
[0.00008090s] factoral(3)->6
[0.00019350s] factoral(4)->24
[0.00022020s] factoral(5)->120
[0.00023160s] factoral(6)->720
720
-
1、 注意,如果原始函数需要传递参数,需要定义内部嵌套函数来接收参数,外部函数只接收被装饰的函数对象。
-
2、 出现嵌套函数就注意闭包啦!像内部函数result = func(n)之所以可以被使用,就是因为func成为了一个自由变量哦,如果不知道,赶紧回去看看嵌套函数与闭包吧。
-
3、内部函数返回真实的结果,外部函数一般都是返回内部函数取代被装饰函数
-
4、还记得不,这里等价于factoral=clock(factoral),这两个factoral已经不是一个对象啦,赋值语句左侧获取的新factoral对象其实就是披着羊皮(factoral)的狼🐺(clocked)。你不信可以看看现在的factoral的真实名字:
>>>factoral.__name__ 'clocked'
你看,我就说吧。突然想起来一个成语:偷天换日
如果你对这种挂羊头卖狗肉的factoral感到耻辱,实时上也存在一定的补救措施functools.wraps,functools.wraps是标准库中拿来即用的装饰器之一,可以把相关的属性从func复制到clocked中,此外还能正确处理关键字参数。
import time
import functools
def clock(func):
name = func.__name__
@functools.wraps(func) # 注意这里哦,就加了这一个地方
def clocked(*arg,**kwargs):
n = 1
if arg:
n = int(arg[0])
start = time.perf_counter()
result = func(n)
cost_time = time.perf_counter() - start
print("[{0:.8f}s] {1:s}({2:d})->{3:d}".format(cost_time, name, n,
result))
return result
return clocked
现在我们再来看一下这个被装饰对象的name
>>>factoral.__name__
'factoral'
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
——未完待续——
欢迎关注我的微信公众号