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