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函数,名字hello
与wrapped
函数绑定。此时名字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 的装饰器可以分为两种:
- 不带参数的装饰器
- 带参数的装饰器
在 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")
在调用时,原本传入hello
或hello2
函数的参数会首先传入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'
函数hello
被makeitalic
装饰后,它的函数名称已经改变了:
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。