定义

Java有一个装饰器设计模式主要就是动态地给一个对象添加一些额外的职责
python也是这样,但python中函数可以作为参数传递,也可以返回函数(闭包)

例子

一个最简单的装饰器

# 一个简单事例
def test01(func):
    def wraps():
        print('my first decorator!')
        return func()
    return wraps
@test01
def mytest01():
    print('haha...')
mytest01()
my first decorator!
haha...

看上面的代码,test01是一个最简单的装饰器,来简单解读一下这个代码。
程序从mytest01()开始运行,因为@test01的存在,运行mytest01()的时候,解释器会将这里解释为,
mytest01 = test01(mytest01)
mytest01()
然后程序运行,return wraps 返回wraps闭包,这时的mytest01就是返回的wraps闭包
mytest01()运行,wraps()闭包运行,首先完成打印my first decorator!,实现额外添加的功能
然后return func()这里返回的是运行结果,所以func会运行,完成mytest01本有的功能,最后程序结束。

装饰函数带参数

ok,上面的代码中可以看到装饰的函数是不带参数,有参数怎么办呢?特别是装饰器要考虑到通用性,装饰的函数参数个数不确定,这时候,*args,**kwargs就很有用了

def test02(func):
    def wraps(*args, **kwargs):  # 注意这里就要带上*args和**kwargs了
        print('haha...')
        return func(*args, **kwargs)
    return wraps
@test02
def mytest02(a, b):
    print(a+b)
@test02
def mytest03(a, b, c):
    print(a*b*c)
mytest02(1,2)
mytest03(2,3,4)
haha...
3
haha...
24

来解释下为什么要在wraps参数中写上*args和**kwargs
按照上面的第一个例子的解释,这里以mytest02为例
mytest02 = test02(mytest02)
mytest02 = wraps
mytest02(1,2)就相当于执行wraps(1,2),所以wraps参数中不写上*args和**kwargs就会报
TypeError: wraps() takes 0 positional arguments but 2 were given
没错,这里报wraps()就很好理解了
ps:这里注意被修饰函数的参数是正常写,不用写成*args和**kwargs

装饰器带参数

上面已经解决了装饰函数有参数如何解决的问题,现在来解决装饰器需要参数的问题
这个问题的需求也很贴合实际,运用同一装饰器,不同的装饰函数可能需要适当的调整

def test03(name):
    def dec(func):
        def wraps(*args, **kwargs):
            print('This is ' + name)
            return func(*args, **kwargs)
        return wraps
    return dec
@test03('01')
def mytest04():
    print('haha...')
@test03('02')
def mytest05():
    print('haha...')
mytest04()
mytest05()
This is 01
haha...
This is 02
haha...

上面的代码可以看到虽然用的同一个装饰器,但因为装饰器接受了参数所以打印出的值可以定制化了。
还是来进行分析,加上装饰器后mytest04()进行了什么样的转化
mytest04 = test(name)(mytest04)
     \= dec闭包
     \= wraps闭包
这里wraps的闭包中就有name和mytest04这两个参数了,再mytest04()运行得到的闭包,就实现了装饰器传参




再来回想这三层函数,每层函数的作用:

  1. test03(name) 作为最外层的函数,主要起到了传递装饰器参数的作用,返回闭包的作用
  2. dec(func) 这里就与前面无参的最外层函数就一样了,起到传递被装饰函数,返回闭包的作用
  3. wraps(*args, **kwargs)这里是最核心的函数,实现增加的功能和运行被装饰的函数

基于类实现的装饰器

装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了call()方法,那么这个对象就是callable的。

下面会对callable对象进行一个解释

无参数的类装饰器

class MyFirstDec():
    def __init__(self, func):
        self.func = func
    def __call__(self):
        print('This is a classdec!')
        self.func()
@MyFirstDec
def mytest06():
    print('haha...')
mytest06()
This is a classdec!
haha...

带参数的被装饰函数的无参数装饰器

class MyFirstDec():
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print('This is a classdec!')
        self.func(*args, **kwargs)
@MyFirstDec
def mytest06(a, b):
    print('haha...'+str(a+b))
mytest06(1,2)
This is a classdec!
haha...3

带参数的装饰器

class MyFirstDec():
    def __init__(self, name, msg):
        self.name = name
        self.msg = msg
    def __call__(self, func):
        def wraps(*args, **kwargs):
            print(self.msg)
            print('This is a %s classdec!'%self.name)
            func(*args, **kwargs)
        return wraps
@MyFirstDec(name = 'kid', msg = 'lala')
def mytest06(a, b):
    print('haha...'+str(a*b))
mytest06(2, 3)
lala
This is a kid classdec!
haha...6

从上面三个基于类的装饰器可以看出,带参数的装饰器和不带参数的装饰器有较大区别
在构造函数中传递的不再是要被装饰的函数,而是装饰器中的参数
这里func为什么不能往构造函数里面传,我的理解是,装饰器不写参数和写参数相当于两种模式了,不写参数,call就不需要接受参数了

callable

  • callable() 是一个python的内置函数,用于检查一个对象是否是可调用的。如果返回True,object仍然可能调用失败;但如果返回False,调用对象ojbect绝对不会成功。
    对于函数, 方法, lambda 函式, 类, 以及实现了 call 方法的类实例, 它都返回 True。
  • call : 如果在类中实现了 call 方法,那么实例对象也将成为一个可调用对象,这就让实例柯一祥方法一样调用,意味着 x() 与 x.call() 是相同的
class test():
    def __init__(self):
        self.a = 10
    def __call__(self, num):
        print(num > self.a)
mytest = test()
mytest(2)
False

functools.wraps

其实上面的装饰器都是存在问题的,问题在哪里呢?
按照上面所说装饰器的原理,以二层装饰器来举例,
mytest02 = test02(mytest02)
mytest02 = wraps
这里执行mytest02就相当于执行了wraps
这时候我们调用的mytest02,mytest02就已经变成wraps函数的了
这时候再来查看mytest02的name属性,就发现已经变为wraps了

def test04(func):
    def wraps(*args, **kwargs):
        print('haha')
        return func(*args, **kwargs)
    return wraps

def mytest06():
    print('test06')

@test04
def mytest07():
    print('test07')

a = mytest06
b = mytest07
print(a.__name__)
print(b.__name__)
mytest06
wraps

所以,需要把原始函数的name等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错
当然我们可以写wrapper.__name__ = func.__name__这样的代码,但python内置的functools.wraps就能完成这件事

import functools
def test05(func):
    @functools.wraps(func)
    def wraps(*args, **kwargs):
        print('haha')
        return func(*args, **kwargs)
    return wraps

def mytest08():
    print('test06')

@test05
def mytest09():
    print('test07')

a = mytest08
b = mytest09
print(a.__name__)
print(b.__name__)
mytest08
mytest09

从上面的代码就可以看到,运行已经不再有问题了
所以wrapper()的前面加上@functools.wraps(func)即可
但wraps功能并不是很完善,数的签名和源码还是拿不到的,要彻底解决这个问题可以借用第三方包,比如wrapt

内置的装饰器

内置的装饰器和普通的装饰器原理是一样的,只不过返回的不是函数,而是类对象
常用的有三个

  1. @property
    使调用类中的方法像引用类中的字段属性一样。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式。遵循了统一访问的原则。
    相当于没用装饰器你要class.func()来调用方法,有了装饰器你可以class.func来调用方法和调用属性一样
    经过@property装饰过的函数返回的不再是一个函数,而是一个property对象。
  2. @staticmethod
    将类中的方法装饰为静态方法,即类不需要创建实例的情况下,可以通过类名直接引用。到达将函数功能与实例解绑的效果。
    这个就相当于java中的静态方法吧
  3. @classmethod
    类方法的第一个参数是一个类,是将类本身作为操作的方法。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。
    python @classmethod 的使用场合

第三方包

  1. decorator
    装饰器加强包,可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器
    也可以使用它自带的@decorator装饰器来完成你的装饰器
    decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)
from decorator import decorate
def wraps(func, *args, **kwargs):
    print('haha')
    return func(*args, **kwargs)
def test06(func):
    return decorate(func, wraps)

@test06
def mytest10():
    print('mytest10')

mytest10()
haha
mytest10
from decorator import decorator
@decorator
def test07(func, *args, **kwargs):
    print('haha')
    return func(*args, **kwargs)
@test07
def mytest11():
    print('mytest11')
mytest11()
haha
mytest11
  1. wrapt
    我觉得这个包,按照说法要是自己写生成器的话,用这个模块是最方便的,也是最好的
    使用wrapt实现的装饰器不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误
# 无参数装饰器
import wrapt
@wrapt.decorator
def wrapper(func, instance, args, kwargs):
    print('haha')
    return func(*args, **kwargs)
@wrapper
def mytest12():
    print('mytest12')
mytest12()
haha
mytest12

使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs),注意第二个参数instance是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值你能够更加灵活的调整你的装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号

# 带参数装饰器  
def test08(name):
    @wrapt.decorator
    def wrapper(func, instance, args, kwargs):
        print(name)
        return func(*args, **kwargs)
    return wrapper
@test08('kid')
def mytest13():
    print('mytest13')
mytest13()
kid
mytest13

注意 @wrapt.decorator的写的位置

注意

  1. 不要轻易在装饰器外层函数添加操作,比如无参装饰器两层函数,有参装饰器三层函数,尽量是将额外添加的操作写到最里面的那层,外面的操作不好控制(因为返回的是里面函数的闭包,即使同一个对象多次执行,外层函数的操作只会运行一次),容易出错,不要瞎添加。
  2. 有参装饰器和无参装饰器区别较大,特别是类装饰器,注意区别。
  3. @functools.wraps(func)加在调用func函数的上面,然后括号里有func这个参数
  4. 不能装饰@staticmethod 或者 @classmethod
    @staticmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的(比如传入一个callable对象),自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod之前就好了,因为这样装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod是不会出问题的
    @staticmethod
    @logging
    这样就是可行的

一些例题

1.能否写出一个@log的decorator,使它既支持:
@log
def f():
pass
又支持:
@log(‘execute’)
def f():
pass

import functools
def log(flag):
    if isinstance(flag, str):
        def middle(func):
            functools.wraps(func)
            def wraps(*args, **kwargs):
                if flag == 'exec':
                    print('begin call')
                    func(*args, **kwargs)
                    print('end call')
                else:
                    return func(*args, **kwargs)
            return wraps
        return middle

    else:
        functools.wraps(flag)
        def wraps(*args, **kwargs):
            print('begin call')
            flag(*args, **kwargs)
            print('end call')
        return wraps
@log
def f():
    print('haha')
@log('exec')
def h():
    print('hehe')
@log('notexec')
def l():
    print('lala')
f()
h()
l()
begin call
haha
end call
begin call
hehe
end call
lala

这题对装饰器和鸭子类型有一定的理解就比较好理解了

参考

  1. 详解Python的装饰器
  2. 廖雪峰装饰器
  3. python @classmethod 的使用场合
  4. Python装饰器的另类用法
  5. Python call详解

路漫漫其修远兮,吾将上下而求索