跳转至

3.4.装饰器

装饰器简述

有时候,我们需要给一个函数添加新功能。有一种做法是在原函数的基础上添加或修改代码,直接添加新功能,但有时未免太繁琐。如果原函数的代码可以不被修改,在 Python 中,给原函数套上装饰就可以实现这个需求。

假设有一个这样的函数:

def hello():
    return 'hello world'


print("The name of 'hello' function:", hello.__name__)
The name of 'hello' function: hello

现在我们想增强hello()函数的功能,希望给返回加上 HTML 标签,比如<i>hello world</i>,但有一个要求,不改变原来hello()函数的定义。

那么可以这样做:

def makeitalic(func):
    def wrapped():
        return "<i>" + func() + "</i>"

    return wrapped


hello = makeitalic(hello)

要注意的是,makeitalic函数的参数是一个函数,返回的结果是wrapped函数。

hello函数作为参数传入makeitalic函数,makeitalic函数返回wrapped函数,名字hellowrapped函数绑定。此时名字hello代表了一个函数,也就是wrapped函数有了新的名字——hello。再次运行以下代码,可以看出hello函数的名字变了:

print("The name of 'hello' function:", hello.__name__)
The name of 'hello' function: wrapped

事实上,makeitalic函数就是一个装饰器(decorator),它装饰了函数 hello,并返回一个函数,将其赋给hello变量。简而言之,装饰器接收被装饰的函数作为参数,返回一个新函数,新函数和被装饰函数的名字绑定。可见,装饰器本质上是一种高阶函数,用于动态修改函数或类的功能。

装饰器的一般使用形式

Python 为装饰器提供了@decorator形式的语法糖,可以在定义被装饰函数的位置应用装饰器,从而让代码容易阅读,让人立刻意识到使用了装饰器。

Python 的装饰器可以分为两种:

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

在 Python 中,不带参数的装饰器一般这样用:

def decorator(fun):
   def wrapped():
       pass
   return wrapped

@decorator
def func():
    pass

此时,decorator函数就是真正的装饰器,上述代码等价于下面的形式

def func():
    pass
func = decorator(func)

装饰器可以定义多个,离函数定义最近的装饰器先被调用,比如

@decorator_one
@decorator_two
def func():
    pass

等价于:

def func():
    pass

func = decorator_one(decorator_two(func))

装饰器还可以带其他参数,比如:

@decorator(arg1, arg2)
def func():
    pass

此时,decorator并不是真正的装饰器,真正的装饰器由decorator(arg1, arg2)返回。上述代码等价于:

def func():
    pass

func = decorator(arg1, arg2)(func)

要注意的是,装饰器必须返回函数,若返回的不是函数,在之后调用时则会报错:

def makeitalic2(func):
    return "<i>" + func() + "</i>"


@makeitalic2
def hello_2():
    return 'hello world'


try:
    hello_2()
except TypeError as e:
    print(TypeError, e)
<class 'TypeError'> 'str' object is not callable

带参数的被装饰函数

前面的例子中,被装饰的函数hello()是没有带参数的,而以下是一个被装饰函数带有参数的例子:

def makeitalic(func):
    def wrapped(*args, **kwargs):
        ret = func(*args, **kwargs)
        return '<i>' + ret + '</i>'
    return wrapped

@makeitalic
def hello(name):
    return 'hello %s' % name

@makeitalic
def hello2(name1, name2):
    return 'hello %s, %s' % (name1, name2)

hello('Jack')

hello2('Jack',"Lusis")

在调用时,原本传入hellohello2函数的参数会首先传入wrapped函数之中。

带参数的装饰器

上面的例子,我们增强了hello函数的功能,给它的返回加上了标签 <i>...</i>,现在,我们想改用标签 <b>...</b><p>...</p>。是不是要像前面一样,再定义一个类似makeitalic的装饰器呢?其实,我们可以定义一个函数,将标签作为参数,返回一个装饰器,比如:

def wrap_in_tag(tag):
    def decorator(func):
        def wrapped(*args, **kwargs):
            ret = func(*args, **kwargs)
            return '<' + tag + '>' + ret + '</' + tag + '>'
        return wrapped

    return decorator

# 根据 'b' 返回 makebold 生成器
makebold = wrap_in_tag('b')

@makebold
def hello(name):
    return 'hello %s' % name

hello('world')
'<b>hello world</b>'

上面的形式也可以写得更加简洁:

@wrap_in_tag('b')
def hello(name):
    return 'hello %s' % name

多个装饰器

现在,让我们来看看多个装饰器的例子,为了简单起见,下面的例子就不使用带参数的装饰器。

def makebold(func):
    def wrapped():
        return '<b>' + func() + '</b>'

    return wrapped

def makeitalic(func):
    def wrapped():
        return '<i>' + func() + '</i>'

    return wrapped

@makebold
@makeitalic
def hello():
    return 'hello world'

上面定义了两个装饰器,对 hello 进行装饰,上面的最后几行代码相当于:

def hello():
    return 'hello world'

hello = makebold(makeitalic(hello))

调用函数 hello:

hello()
'<b><i>hello world</i></b>'

基于类的装饰器

前面的装饰器都是一个函数,其实也可以基于类定义装饰器,看下面的例子:

class Bold(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return '<b>' + self.func(*args, **kwargs) + '</b>'

@Bold
def hello(name):
    return 'hello %s' % name

hello('world')

可以看到,类 Bold 有两个方法:

__init__():它接收一个函数作为参数,也就是被装饰的函数

__call__():让类对象可调用,就像函数调用一样,在调用被装饰函数时被调用

还可以让类装饰器带参数:

class Tag(object):
    def __init__(self, tag):
        self.tag = tag

    def __call__(self, func):
        def wrapped(*args, **kwargs):
            return "<{tag}>{res}</{tag}>".format(
                res=func(*args, **kwargs), tag=self.tag
            )
        return wrapped

@Tag('b')
def hello(name):
    return 'hello %s' % name

需要注意的是,如果类装饰器有参数,__init__ 接收参数,而 __call__ 接收 func。

装饰器的副作用

使用装饰器有一个瑕疵:被装饰的函数的函数名称不再是原来的名称。回到最开始的例子:

def makeitalic(func):
    def wrapped():
        return "<i>" + func() + "</i>"
    return wrapped

@makeitalic
def hello():
    return 'hello world'

函数hellomakeitalic装饰后,它的函数名称已经改变了:

hello.__name__
'wrapped'

Python 中的 functools 包提供了一个wraps装饰器,可以消除这样的副作用:

from functools import wraps

def makeitalic(func):
    @wraps(func)       # 加上 wraps 装饰器
    def wrapped():
        return "<i>" + func() + "</i>"
    return wrapped

@makeitalic
def hello():
    return 'hello world'

hello.__name__
'hello'

事实上,装饰器就是闭包的一种应用,但它比较特别,接收被装饰函数为参数,并返回一个函数,赋给被装饰函数的标识符,闭包则没这种限制。

装饰器的应用场景

前面介绍了这么多,我们对装饰器也有了一个大概的认识,但此时,很多初学者心里可能会有一个疑惑——似乎很多时候不用装饰器也没问题,那么到底什么时候需要用到装饰器?

的确,很多时候只针对一两个函数使用装饰器是没有必要的。装饰器一个重要的应用在于,如果很多函数有共同的代码,而这些代码与函数功能的实现又没有关系,那么就可以考虑将这些共同的代码利用装饰器的形式实现。例如,记录函数行为的代码(计时、记录日志、错误警告等)就可以考虑用装饰器来实现。这样可以复用代码,保持代码的简洁,让代码更容易维护。

另一方面,装饰器的灵活性可以带来更优雅或更灵活的实现,例如注入参数(提供默认参数、生成参数)、预处理/后处理等等。

当然,还有一种最直接的应用,如果需要改变函数的功能,但出于一些考虑,又不想修改原函数的代码,那么就可以用装饰器。

最后,关于装饰器的具体应用,可以参考 Python 的官方文档——PythonDecoratorLibrary