python装饰器_python装饰器完全指南之二

带参数的装饰器

上面的讨论中,我们已经涉及到了装饰器函数本身需要参数(注意不是指功能函数带参数)的情况。比如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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值