投稿自 网易游戏运维平台 公众号:neteasegameops
♚作者:吉星高照,网易游戏资深开发工程师,主要工作方向为网易游戏 CDN 自动化平台的设计和开发,脑洞比较奇特,喜欢在各种非主流的领域研究制作各种不走寻常路的东西。
Python的装饰器是面试的常客,因为其写法复杂多变,经常忘记什么地方应该写哪种参数,新手学习起来也经常一头雾水,不怕不怕,看了这一篇你对装饰器的各种用法就全明白了。废话不多说,直接进入主题!
不带参数的函数,不带参数的装饰器
我们先来写一个简单的装饰器,实现将函数运行前后的情况记录下来。
def dec1(func): print(func) def _wrap(): print('before run') r = func() print('after run') return r return _wrap@dec1def f1(): print('call f1')
上面只是定义了两个函数,运行后发现竟然有输出:
<function f1 at 0x7fa1585f8488>
仔细看看,原来是第一个print语句的输出。这说明装饰的函数还没有实际运行的时候,装饰器就运行过了,因为 @dec1相当于单独的一个语句:
dec1(f1)
那我们来正式运行一下f1吧:
f1(1)
输出如下确实达到了预期的效果:
before runcall f1after run
不带参数的函数,带空参数的装饰器
后面我们还想要给装饰器加上参数呢,先试试用这个方式调用会发生什么情况:
@dec1()
输出了错误:
Traceback (most recent call last)<ipython-input-268-01cf93cf6907> in <module> 8 return _wrap 9 ---> 10 @dec1() 11 def f1(): 12 print('call f1')TypeError: dec1() missing 1 required positional argument: 'func'
它说dec1需要接受 func这个参数才行,那我们改改,作为f2函数吧:
def dec2(): def _wrap(func): print(func) print('before run') return func return _wrap@dec2()def f2(): print('call f2')f2()
这下可以了:
<function f2 at 0x7fa1585af2f0>before runcall f2
可是这个结构和原来有点不同了,而且,after run要写在哪里呢?很愁人地又改了一版,把它叫做 f2x吧,对比dec1,又多了一层函数 dec2x_w,开始有点晕:
def dec2x_w(): def dec2x(func): print(func) def _wrap(): print('before run') r = func() print('after run') return r return _wrap return dec2x@dec2x_w()def f2x(): print('call f2x')f2x()
运行一下看看,确实是想要的:
<function f2x at 0x7fa1585af950>before runcall f2xafter run
后面我们就不加before/after run了。
带参数的函数,不带参数的装饰器
函数f2x想要接受参数呢?我们把它叫做 a吧,比较简单,不就是_wrap的参数吗,加上就是了,而且又回到了只有两层函数就可以实现了:
def dec3(func): print(func) def _wrap(param): print(param) r = func(param) return r return _wrap@dec3def f3(a): print('call f3', a)f3(1)
很争气地输出了结果:
<function f3 at 0x7fa158719620>1call f3 1
带参数的函数,带参数的装饰器
下面我们实现一个装饰器,传入一个整数,和函数传入的参数做个加法:
def dec4_w(d_param): print(d_param) def dec4(func): print(func) def _wrap(param): print(param) r = func(param + d_param) return r return _wrap return dec4@dec4_w(2)def f4(a): print('call f4', a)f4(1)
输出1+2=3:
2<function f4 at 0x7fa1585af598>1call f4 3
从调用的装饰器往里看,注意这三层函数的形参,第一层是装饰器的参数,第二层是函数,第三层是函数的参数,很有规律的排列,先记一下这个规律(要考的)。带两个参数的也是一样的,接着写就可以了:
def dec5_w(d_param_1, d_param_2): print(d_param_1, d_param_2) def dec5(func): print(func) def _wrap(param): print(param) r = func(param + d_param_1 + d_param_2) return r return _wrap return dec5@dec5_w(2, 3)def f5(a): print('call f5', a)f5(1)
输出1+2+3=6:
2 3<function f5 at 0x7fa1586237b8>1call f5 6
如果用不定数量的位置参数,就用*args作为形参吧:
def dec6_w(*args): d_param_1, d_param_2, = args print(d_param_1, d_param_2) def dec6(func): print(func) def _wrap(*args): param = args[0] print(param) r = func(param + d_param_1 + d_param_2) return r return _wrap return dec6@dec6_w(2, 3)def f6(a): print('call f6', a)f6(1)print(f6.__name__)
顺便输出了一下f6的函数名:
2 3<function f6 at 0x7fa1586236a8>1call f6 6_wrap
咦!怎么肥四!!!f6怎么是里面那个 _wrap的名字呢?不怕不怕,functools提供了一个 wraps装饰器专治各种不服(在装饰器里面放上另一个装饰器):
from functools import wrapsdef dec7_w(*args): d_param_1, d_param_2, = args print(d_param_1, d_param_2) def dec7(func): print(func) @wraps(func) def _wrap(*args): param = args[0] print(param) r = func(param + d_param_1 + d_param_2) return r return _wrap return dec7@dec7_w(2, 3)def f7(a): print('call f7', a)f7(1)print(f7.__name__)
这下正常输出f7了:
2 3<function f7 at 0x7fa1585f8510>1call f7 6f7
装饰器类(带参数的函数,带参数的装饰器)
用函数做装饰器局限性太多了,用相同的调用方法,把函数f7改成类怎么样?emmm…改造工程有点大,直接看看成品吧:
from functools import wrapsclass dec8_c: def __init__(self, *args): self.d_param_1, self.d_param_2, = args print(self.d_param_1, self.d_param_2) def __call__(self, func): print(func) @wraps(func) def _wrap(param): print(param) r = func(param + self.d_param_1 + self.d_param_2) return r return _wrap@dec8_c(2, 3)def f8(a): print('call f8', a)f8(1)print(f8.__name__)
看看是不是实现了一样的效果:
2 3<function f8 at 0x7fa1585f8048>1call f8 6f8
虽然使用了__call__,但这里的 __init__不能省略(因为它需要知道参数个数),否则会出现这个错误:
Traceback (most recent call last)<ipython-input-276-1634a47057a2> in <module> 14 return dec8 15 ---> 16 @dec8_c(2, 3) 17 def f8(a): 18 print('call f8', a)TypeError: dec8_c() takes no arguments
同时还可以发现,__call__只需要两层函数了,去掉了第二层,直接把 _wrap的函数体往上提了一层!
装饰器类(带参数的函数,不带参数的装饰器)
大概是吃饱了撑着,又想要实现一开始那个不带参数的装饰器了,那就继续敲敲打打一番看看:
class dec9_c: def __init__(self, func): print(func) self.func = func self.__name__ = func.__name__ def __call__(self, param): print(param) func = self.func r = func(param) return r@dec9_cdef f9(a): print('call f9', a)f9(1)print(f9.__name__)
赶快运行看看:
<function f9 at 0x7fa1585f8730>1call f9 1f9
咦,f9的函数名可以直接打印,这下都不用 @wraps了呢!呃,再仔细看看,这写法好像有些不一样啊:
-
dec8_c的init带的是装饰器的参数,可是dec9_c带的是装饰器函数自己! -
所以实际调用的函数名也可以在
init中传给它了哦! -
而且
call函数也简洁了很多,看来有没有参数真的有很大区别呢!
这里先做个总结,装饰器使用函数名形式(不带括号)和使用函数调用形式(带括号和参数)在实现上是不同的,因为前者是函数本身,而后者是从装饰器函数中返回的函数。这也是f2相比 f1缺少了记录after run的原因,因为 dec1直接调用了f2,而 dec2先运行得到函数,再把函数返回去调用f2。用装饰器类就可以解决这个问题,因为它是对
__call__的调用,只需要自己定义一下就可以了。
上面的f9要写两个函数,能不能写得和 f1一样简洁?当然是可以的,使用__new__大法:
from functools import wrapsclass dec9x_c: def __new__(self, func): print(func) @wraps(func) def dec9x(param): print(param) r = func(param) return r return dec9x@dec9x_cdef f9x(a): print('call f9x', a)f9x(1)print(f9x.__name__)
这样就避开了函数调用,不用打call了(定义__call__函数),快看它来了:
<function f9x at 0x7fa158623bf8>1call f9x 1f9x
▼点击成为
社区注册会员 喜欢文章,点个 在看