包装一下
在系列一(→
点我传送
)中我们提到,函数可以作为参数传递,也可以在另一个函数的内部定义并返回出来变成一个新的函数:
defwrap(func1):
print('In wrap')
deffunc2():
print('In func2')
func1()
print('After func1')
print('Return from wrap')
returnfunc2
deffunc():
print('hello')
func2=wrap(func)
# In wrap
# Return from wrap
func2()
# In func2
# hello
# After func1
接收一个函数作为参数,并返回了一个新定义的函数。在里调用了接收的函数参数。通过打印结果我们可以清楚得跟踪到函数的执行流程。那么,在函数中传递函数、定义函数、返回函数有什么实际意义吗?假设我们想统计一些函数的执行时间,我们可以在函数体的开头和结尾分别获取一个时刻值,再相减即可得到这段函数执行的时间:
importtime
deffunc1():
start=time.time()
# 实际函数体
# 这里为了体现时间直接休眠1秒
time.sleep(1)
end=time.time()
print('Time consumed: {}'\
.format(end-start))
func1()
# hello
# Time consumed: 1.000394582748413
试想一下,如果有100个这样的函数都需要统计时间,上述写法的弊端就体现出来了,重复性代码。此外,上述代码也破坏了原函数的封闭性。有没有什么办法能够一劳永逸解决这个问题呢?统计时间的流程是这样的,先获取起始时间,再执行目标函数,再获取结束时间。这个流程是不是和上面例子里的一样呢?按照上面方式改写一下:
importtime
defwrap(func):
defnew_func():
start=time.time()
func()
end=time.time()
print(
'Time consumed: {}'\
.format(end-start)
)
returnnew_func
deffunc1():
time.sleep(1)
new_func1=wrap(func1)
new_func1()
# Time consumed: 1.0008351802825928
这样,我们相当于为包装了一层(所以叫),统计了一下时间。这样,有再多的函数需要统计时间,也只是在不改变函数内部的基础上增加一行代码包装即可:
new_func2=wrap(func2)
new_func3=wrap(func3)
new_func4=wrap(func4)
利用这一特性,我们可以很方便得扩展代码功能。
@
Python为上述函数式特性增加了一个语法糖实现:装饰器。我们可以通过@符号来为一个函数指定一个装饰函数。在上例中,我们可以在定义位置指定使用装饰器,然后直接用调用就是新函数的结果:
@wrap
deffunc1():
time.sleep(1)
func1()
# Time consumed: 1.0009453296661377
相当于这样的过程。是不是更显简洁了?
带参数的func1
通常,函数都是有参数的,要装饰的函数自然也不例外,那这些函数如何传递呢?答案是利用可变参数传递(→点我传送):
importtime
defwrap(func):
defnew_func(*args,**kwargs):
start=time.time()
func(*args,**kwargs)
end=time.time()
print(
'Time consumed: {}'\
.format(end-start)
)
returnnew_func
@wrap
deffunc1(a,b):
print(a)
time.sleep(1)
print(b)
func1('hi',b='hello')
# hi
# hello
# Time consumed: 1.000152349472046
这里可能有人会有疑问,为什么可变参数加到了上面而不是上面?因为最终实际是用代替了函数,真正调用执行的是函数,自然参数要传递给它咯。由于Python存在可变参数,我们大可不必担心函数会遗漏某些参数,并且原始函数的参数列表也丝毫没有改变。自然的,的返回值也可以在中返回出来:
importtime
defwrap(func):
defnew_func(*args,**kwargs):
start=time.time()
res=func(*args,**kwargs)
end=time.time()
print(
'Time consumed: {}'\
.format(end-start)
)
returnres
returnnew_func
@wrap
deffunc1(a,b):
print(a)
time.sleep(1)
returnb
res=func1('hi',b='hello')
# 'hi'
# Time consumed: 1.000823974609375
print(res)
# 'hello'
带参数的@
有时候,我们不止需要统计时间,可能我们还需要让某个函数重复执行几次,或者说,我们需要给传递一些参数来控制装饰的过程。例如,想让执行次,那么我们需要再在之上再包装一层,专用于接收参数,再把返回出去:
deftimes(n=5):
def_wrap(func):
defnew_func(
*args,
**kwargs
):
foriinrange(n):
func(*args,**kwargs)
returnnew_func
return_wrap
这样我们可以为传递参数来指明究竟要调用几次:
@times(3)
deffunc1(a):
print(a)
func1(a='hello')
# hello
# hello
# hello
# times自带默认参数
@times()
deffunc1(a):
print(a)
func1(a='hi')
# hi
# hi
# hi
# hi
# hi
细心的朋友可以看到,这里使用了闭包(什么是闭包?→传送门)。
@的组合
一个函数可以应用多个装饰器。这些装饰器依照书写位置自下而上调用,例如我们利用上面的和来定义一个函数:
@times(3)
@wrap
deffunc():
time.sleep(1)
print('hi')
func()
# hi
# Time consumed: 1.0003962516784668
# hi
# Time consumed: 1.0006020069122314
# hi
# Time consumed: 1.0000085830688477
@wrap
@times(3)
deffunc():
time.sleep(1)
print('hi')
func()
# hi
# hi
# hi
# Time consumed: 3.0017807483673096
看到区别了吗?下方的装饰器会先被调用。将最后例子流程用函数调用方式来说明是这样的 :
func=wrap(_wrap(func))
# _wrap中有times的闭包值5
无处不在的@
装饰器在Python中无所不在,例如,在Flask框架中,我们可以利用装饰器来定义HTTP路由:
fromflaskimportFlask
app=Flask(__name__)
@app.route('/',methods=['GET'])
defhandle():
return'hello'
在类中可以定义静态方法:
classA:
@staticmethod
defm():
pass
等等。
最后一个问题
前面提到过,函数可以定义帮助文档,并通过查看(或通过查看一个函数的文档。现在来看一下经过装饰器装饰后的函数文档变成了什么:
deffunc1(a):
'This is a func'
print(a)
print(func1.__doc__)
# 'This is a func'
@times()
deffunc1(a):
'This is a func'
print(a)
print(help(func1))
# None
没了?。。再看一下这个函数叫什么:
print(func1.__name__)
# new_func
这是因为经过装饰的函数已经变成了装饰器中定义的函数,所以不论函数名称还是文档都已经变成新函数的相应内容了。那么,如何让经过装饰器的函数能够保留旧函数的这些内容呢?利用标准库中的装饰器:
importfunctools
deftimes(n=5):
def_wrap(func):
@functools.wraps(func)
defnew_func(
*args,
**kwargs
):
foriinrange(n):
func(*args,**kwargs)
returnnew_func
return_wrap
@times()
deffunc1(a):
'This is a func'
print(a)
print(func1.__doc__)
# This is a func
print(func1.__name__)
# func1
友情链接: https://vonalex.github.io/
欢迎关注 它不只是Python
领取专属 10元无门槛券
私享最新 技术干货