带参数的装饰器
上面的讨论中,我们已经涉及到了装饰器函数本身需要参数(注意不是指功能函数带参数)的情况。比如functools.wraps的实现是:更新被它装饰wrapper函数的一些元属性。我们从装饰器是如何工作的一节中已经看到,它必然会接受wrapper对象作为其一个参数,这个由模块加载器传入。而另一个参数,即功能函数对象(这里为func),就需要我们手动传入了。
要写一个带参数的装饰器,可以按以下步聚:
1. 按照第1节的方法,写一个不带参数的装饰器(inner decorator)
2. 加上functools.wraps
3. 在上述装饰器之上,再加一层函数(outer decorator)
import functools# 装饰器函数名要调整到最外层def snoop(counter): def decorator(func): # functools.wraps始终贴近替换函数 @functools.wraps(func) def wrapper(number): print("passed in param is ", number)# 装饰器函数的参数可以在任何地方访问 result = func(counter + number) print("buggy_incr_by returned ", result) return wrapper return decorator@snoop(10)def buggy_incr_by(number): import random return numberbuggy_incr_by(3)
4. 调整装饰器函数命名。inner decorator可以使用任意的名字;而最外面一层的装饰函数(outer decorator),是我们将要在代码中的其它地方使用的,因此应该是一个能反应装饰器功能的名字。
什么情况下需要带参数的装饰器呢?比如,你的功能函数需要从连接池中获取一个连接,再在这个连接上执行操作:
def myfunc(connection, *args): # do something with the connection pass
你可以在每次调用myfunc前都去手动获取这个连接(以及错误处理),也可以通过一个装饰器去获取连接,再塞给myfunc。由于这个参数是装饰器自动塞进来的,因此我们在调用myfunc时,就不需要传入connection,而只传其它参数。但是,myfunc的签名仍然不能少了这个connection形参。
装饰器不替换功能函数的情况
有时候我们希望通过装饰器来实现信号处理函数的注册。这里的装饰器写法不太一样。
def on(event): def decorator(func): # 将信号处理函数func绑定到event事件上。 register需要自己实现 register(event, func) return decorator@on('user_login')def validate_user(): pass
信号处理函数注册一般接受两个参数,一个是事件,一个是处理函数。我们先通过一个带参的装饰器(这里是函数on),将事件作为参数传入,并构造出一个新的装饰器函数,加载器将调用这个函数,并传入功能函数对象(在上述例子中是validate_user)。
注意这里装饰器并没有返回任何对象,所以装载器并没有将功能函数替换掉。这应该是Python内部实现上的一个约定。这里也可以返回原函数,但是并没有什么意义。
尴尬的self对象
前面提过一句,在模块加载时,加载器就完成了功能函数的替换,此时未发生变量绑定。这会导致多出来self或者缺失self对象的问题。
3.1 self对象缺失的情况
在第5节中提到的场景,如果我们用装饰器来装饰一个实例方法,会发生什么样的情况?
def on(event): def decorator(func): register(event, func) return decoratorclass Foo: def __init__(): self.name = 'foo' @on('user_login') def validate_user(self): # print(self.name) pass
注意第7号进行信号绑定时,绑定会成功,但此时user_login这个信号是绑定在Foo这个对象(在Python中,class本身也是对象,也存在于内存中,这一点跟c++不一样),而不是未来才实例化的一个对象上。因此,当信号发生,你的信号处理机制调用validate_user时,实际调用的是类方法Foo.validate_user。这意味着什么?如果你去掉第11行的注释,此时会报错:程序会把后面的参数传递给self(如果有的话)。
3.2 多出来的self对象
上述例子并不意味着装饰器绝对不可以用于装饰实例方法。在下面的例子中,我们会看到self又会被传递,反而是多出来了:
import functoolsdef snoop(counter): def decorator(func): @functools.wraps(func) def wrapper(number): print("passed in param is ", number) result = func(counter + number) print("buggy_incr_by returned ", result) return wrapper return decoratorclass Foo: def __init__(self): self.counter = -10 @snoop(10) def buggy_incr_by(self, number): import random return self.counter + numberfoo = Foo()foo.buggy_incr_by(5)Traceback (most recent call last): File "", line 1, in TypeError: wrapper() takes 1 positional argument but 2 were given# --output--
在上面的代码中,当我们将snoop绑定到函数buggy_incr_by时,装载器无法将此时尚不存在的self对象隐式地传给snoop(就象它曾经隐式地将func传给decorator那样);而在通过foo对象来调用bugg_incr_by时,解释器则自动将self对象作为函数的第一个参数传入,如此一来,便多了一个参数。这就是上面的错误的根源。
这种情况下,我们要调整wrapper的参数列表,增加一个self对象,并将这个参数放在第一序位,传递给功能函数(因为功能函数作为类方法,需要self作为第一参数):
import functoolsdef snoop(counter): def decorator(func): @functools.wraps(func) # 这里增加了self形参。因此这个装饰器只能用以类方法的装饰 def wrapper(self, number): print("passed in param is ", number) # 在这里我们把self对象传进去 result = func(self, counter + number) print("buggy_incr_by returned ", result) return wrapper return decoratorclass Foo: def __init__(self): self.counter = -10 @snoop(10) def buggy_incr_by(self, number): import random return self.counter + numberfoo = Foo()foo.buggy_incr_by(3)
上述例子还给我们一个推论,即可以将装饰器参数、或者全局变量(如果这样有意义的话),推送给功能函数,即在上述代码段中的第8行,这里除了self, number之外,还可以推送其它参数,当然,func本身的签名也需要更改。这就是我们前文第2节埋下的伏笔:“这个wrapper函数一般接受跟功能函数一模一样的参数。例外情况在下文中叙述。”
当然,我们也可以更一般化地定义wrap函数:
import functoolsdef snoop(counter): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return wrapper return decorator
上述定义可以作为一个更通用的模板来使用。
再谈执行顺序
在前面我们没有对带参数的装饰器做过多的讨论。但是了解带参数的装饰器的执行顺序也是很重要的。只有了解所有相关方的执行顺序,我们才知道能够传什么样的参数给装饰器--这些参数必须在传递之前已经存在。
我们再看一次带参数的装饰器的例子:
import functoolsdef snoop(counter): def decorator(func): @functools.wraps(func) def wrapper(number): print(f"{func.__name__} is called with {number}") result = func(counter + number) print(f"inner decorator(without params) is called, params are {func}") return wrapper print(f"outer decorator(with params) is called with", counter) return decorator@snoop(10)def buggy_incr_by(number): import random return number
执行上述代码(注意我们并没有调用bugg_incr_by),会得到如下输出:
outer decorator(with params) is called with 10inner decorator(without params) is called, params are
输出说明了一切:
1. 加载器首先执行最外层的装饰器函数(在例子中是snoop),并传入参数10;
2. 上述执行结果是另一个装饰器函数,加载器接着调用这个装饰器函数(在例子中是decorator),并传入参数buggy_incr_by
在第13行,我们传入的参数是常量10;如果传入一个变量会怎么样?你必须得保证这个变量在本模块加载时已经声明。因此,我们不可以给它传入类似于self这样的变量--因为self对象在这时往往并不存在。
装饰器可以堆叠。当堆叠发生时,它们的调用顺序如何呢?关于这一点,其实只要了解装饰器的触发机制,就不难得出结论。装饰器是模块加载器执行的,因此是由上至下(代码顺序)执行的。
![4c16adcf82b8b120b717a5a61df2bd31.png](https://img-blog.csdnimg.cn/img_convert/4c16adcf82b8b120b717a5a61df2bd31.png)