Python装饰器decorator的坑

  1. 装饰器内部执行顺序

网上说2层装饰器的多,说3层的不多。所以先简单说下2层装饰器的关键点,有助于理解3层装饰器。Python装饰器定义时本质上是嵌套函数,但是读取时有特殊之处。先看下面例子的运行结果,和预想的执行顺序有些不同。

def b(func):
    print('b')
    def c():
        print('c')
        func()
        # return func()
    return c

@b
def z():
    print('z')

print('-----')
z()
print('-----')
z()
# -=> b
# -=> -----
# -=> c
# -=> z
# -=> -----
# -=> c
# -=> z

在读取@b的时候就运行了b函数。相当于运行了

z = b(z)
# 执行的结果是return c,也就是说,现在
# z = c
# 所以,调用 z 时候的参数,都会传给 c

所以以后再调用z()的时候,实际上执行的是b(z),其中z被当作参数func = z传入。我们知道内层函数可以访问外层函数的变量,内层函数对z做什么都行,内层函数也不一定要return z,但是外层函数b一定要返回内层函数c。

注意,实际上在读取时的运行,并没有传入z函数,因为还没定义!没有传入任何参数给b函数!b函数是在没有func=z的情况下运行的!要等到调用z函数的时候,才会去传入传入参数!上面说的z = b(z)z相当于只是函数名,而不是函数本身!z没有被调用,所以内层函数也不会被调用,因为没传z(*args)进去。

所以,不要在第一层函数里对func做任何事情,因为在读取@b的时候,func没被传入!

这也是为什么装饰器至少要2层函数,因为第一层函数就是个壳函数,在读取的时候已经执行了,以后不会再被执行。因为运行结果是z = c

如果只有一层装饰器,脚本读取@b后就没有了,再调用z()就只有b的运行结果,而不会运行z了;如果你在结尾return func,就等于z = z,之后再调用z()就会失去装饰器效果,只是调用本身了。(如果你希望在脚本载入的时候就执行什么,这是一个奇特的替代方案)


  1. 3层内嵌装饰器,带参数的装饰器

再说一下3层嵌套,这是用于给装饰器添加参数的(不是给目标函数的,是给内层代码用的)。

def a(*param):
    print('a')
    def b(func):
        print('b')
        def c(*args, **kwargs):
            print('c')
            print(*param)
            func(*args, **kwargs)
            # return func(*args, **kwargs)
        return c
    return b

@a('param')
def z():
    print('z')

print('-------')
z()
print('-------')
z()
# -=> a
# -=> b
# -=> -------
# -=> c
# -=> param
# -=> z
# -=> -------
# -=> c
# -=> param
# -=> z

相当于先执行a(*args, **kwargs) = a(*param)执行结果是a(*args, **kwargs) = b(func),然后继续运行,结果是a(*args, **kwargs) = c(*args, **kwargs),这时外层函数的变量*paramfunc都可以被最内层函数访问。

3层装饰器时,前2层都会在读取脚本时执行掉,剩下最内层的核心代码等在调用目标函数时执行。

所以核心代码要放在最内层,建议外面两层放代码的时候,只能是属性和方法这类会保留在内存中的,而不是单纯的执行一次没保存结果的。常用于传递的是可选参数的情况1。因为只会在最初执行一次。


  1. 2层的带参数的装饰器

另外,func默认传递到@decorator这个函数中(在例子里是@a),就有点相似实例中,self默认第一个传递到 实例方法 中一样;不一样之处是实例方法在你输入的几个参数之前加一个self,而装饰器这里是只能传递一个func,如果你用()里输入了参数,会被看作函数调用,而func会被传递到这个调用结果中(即下一层函数)。所以要接受func参数的装饰器,调用时不能加()。如果参数有默认值,不传递参数也能获得默认值。而且不能手动传入func,因为程序还没读到那行,func未定义!

2层装饰器的情况:

# 装饰器b 可以接受参数p1,有默认值。
# 调用时,不输入参数,可获得默认值。但是如果调用时输入参数,会出错!
def b(func, p1='p1'):
    print('b')
    def c(*args, **kwargs):
        print('c')
        print(p1)
        func(*args, **kwargs)
        # return func(*args, **kwargs)
    return c

@b
def z():
    print('z')

print('------')
z()
# -=> b
# -=> ------
# -=> c
# -=> p1
# -=> z

@b(y, p1='p2') # 无法手动传入y函数,因为还未定义!
def y():
    print('y')
# -=> NameError: name 'y' is not defined

@b(p1='p2') # 不传入y函数,会出现缺少参数
def y():
    print('y')
# -=> TypeError: b() missing 1 required positional argument: 'func'

2层装饰器的情况,func函数只能被传到第一层,如果还想接受其他输入参数,还不出错,就要借助functools.partial来实现1。这种方法我不推荐。

**推荐3层装饰器接受额外参数!**第1层接受额外参数,用(*args)调用第一层函数,所以第2层就接受到了func函数。这个装饰器使用时,即使不输入参数,也要用()调用,保证第2层接受到func。


  1. 同时使用多个装饰器

如果同时使用多个装饰器,执行顺序是从内到外。

@a
@b
@c
def z():
    pass

相当于z = a(b(c(z))),先执行c(z)然后b(c(z))然后a(b(c(z)))。调用时相当于a(b(c(z)))()


  1. 类装饰器(是个坑)

2类class装饰器,具有灵活度大、高内聚、封装性等优点。使用在普通函数上依靠__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。还有__get__方法,用于装饰 实例方法,保证self连接到正确的实例(被装饰方法的实例)以使用__call__。有时希望返回一个可调用的实例,或需要装饰器可以同时工作在类定义的内部和外部3。用类装饰器 装饰实例方法时,第一层参数传给__init__并初始化实例(不懂为什么__init__可以找到正确的self实例),第二层参数传给__call__函数。(实际上也可以把__new__重载了接受第一层参数,实例化的时候是先调用__new__创建实例,如果实例被创建,就自动调用__init__。但是,__init__不能加一层wrapper函数,因为 __init__不允许有返回值!)
被装饰的实例方法变成了装饰器的__call__(self,*args)调用。但是,在调用__call__时,先需要 描述器 根据__get__来把self参数和实例instance绑定在一起,如果没有重载__get__去绑定,那么self参数就不会被和 传入的目标函数的实例 绑定在一起3,而一个实例用.连接外部函数的时候,会变成unbound method,导致不会自动传入self参数,最后导致传入的*args占了self的位置,出现少一个参数的TypeError,详情在这 Python添加和绑定方法method到实例instance的小细节4。在调用目标函数时手动传入目标函数实例也可以,但是在脚本运行前,经常不知道目标函数名 或者不符合编程习惯,导致容易出错或不优雅。

下面这个例子展示了__init____get____call__的调用顺序 和对应的实例。代码改编自3

import types
from functools import wraps

class Profiled:
    def __init__(self, func):
        print('init self= ',self)
        print('init func= ',func)
        wraps(func)(self)

    def __call__(self, *args, **kwargs):
        print('call self= ',self)
        return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, cls):
        print('get self= ',self)
        print('get instance= ',instance)
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)


print('------before def add-------')

@Profiled
def add(x, y):
    return x + y

print('-----before class Spam--------')

class Spam:
    @Profiled
    def bar(self, x):
        print('bar self= ',self, x)

print('-----before s = Spam()--------')
s = Spam()
print('------before s.bar(3)-------')
s.bar(3)

# -=> ------before def add-------
# -=> init self=  <__main__.Profiled object at 0x0000017526B65C70>
# -=> init func=  <function add at 0x0000017526BA8430>
# -=> -----before class Spam--------
# -=> init self=  <__main__.Profiled object at 0x0000017526BAD490>
# -=> init func=  <function Spam.bar at 0x0000017526BA9040>
# -=> -----before s = Spam()--------
# -=> ------before s.bar(3)-------
# -=> get self=  <__main__.Profiled object at 0x0000017526BAD490>
# -=> get instance=  <__main__.Spam object at 0x0000017526BAD4F0>
# -=> call self=  <__main__.Profiled object at 0x0000017526BAD490>
# -=> bar self=  <__main__.Spam object at 0x0000017526BAD4F0> 3

可以看出__init__在每次读取装饰器的时候运行一次,给了自身实例一个wrapped(func)(self)self.__wrapped__。之后就不再执行。
接下来调用目标函数bar时,先执行__get__self和目标函数实例s绑定在一起(实际上,这里的self虽然写着是Profiled object,但在调用的时候,执行的是__call__方法。)所以就等于把self.__call__(self,*args)方法绑定到instance目标函数实例s上,成为目标函数实例s的buound method。最后传入参数(self=s, 3)执行self.__wrapped__(*args = s, 3)。(__call__把自己的self吃掉了,返回的是和原本func一样的__wrapped__,用wrap为了保留func的元信息)

也可以考虑绑定Profiled.__call__(self,*args)类函数,不会自动传入参数,直接返回func。保留元信息,用给__call__加@wraps装饰器?
其实把带参数的装饰器应用到实例instance上,主要考虑self实例参数在哪里接受,正确传入参数就好,该补上参数的就补上就好。

避免使用装饰器类吧。
Decorator inside Python class5提供了多种方法,列出了各种情况,但是里面的坑没仔细讲。


  1. @functools.wraps(func)保留原始函数的元数据

@functools.wraps(func)可以保留原始函数的元数据,比如func.__name__之类的。用在最内层函数的def的上一行。一般情况都会加上用一下。

import functools
def a():
    def b(func):
    	@functool.wraps(func)
        def c(*args, **kwargs):
            func(*args, **kwargs)
            # return func(*args, **kwargs)
        return c
    return b

  1. 在类内部定义装饰器

要注意的只是self的位置;还有使用前,需要先实例化,除非这个装饰器是类方法@classmethod。调用装饰器要写出实例或类的名字。

class A:
    def decorator1(self, func):
        def wrapper(*args, **kwargs):
            print('Decorator 1')
            return func(*args, **kwargs)
        return wrapper
    
    @classmethod
    def decorator2(cls, func):
        def wrapper(*args, **kwargs):
            print('Decorator 2')
            return func(*args, **kwargs)
        return wrapper

a = A()

@a.decorator1
def spam():
    print('spam')

spam()

@A.decorator2
def grok():
    print('grok')

grok()

# -=> Decorator 1
# -=> spam
# -=> Decorator 2
# -=> grok

需要注意的是,这2个方法装饰器不能在类A的内部使用,因为在A实例化的过程中,需要先执行装饰器,会导致类A或实例a没有定义的情况。例子改编自9.8 将装饰器定义为类的一部分 - python3-cookbook



  1. 9.6 带可选参数的装饰器 - python3-cookbook ↩︎ ↩︎

  2. 理解 Python 装饰器看这一篇就够了 - foofish.net ↩︎

  3. 9.9 将装饰器定义为类 - python3-cookbook ↩︎ ↩︎ ↩︎

  4. Python添加和绑定方法method到实例instance的小细节 ↩︎

  5. Decorator inside Python class ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值