python-04-01-装饰器

  • 目录
  • 装饰器

Python装饰器,英文decorator, 以被装饰函数作为参数对其装饰。这里会用到函数,我们可以像使用变量一样使用函数,函数有对象一致特性。

  • 函数可以被赋值给其它变量
  • 函数可以内部再定义函数
  • 函数可以作为参数传递给另外一个函数
  • 函数可以直接返回给外层函数

简单函数进行装饰

有这么一个函数

1
2
def hello():  
return 'hello world'

我们想在不修改原函数的情况下,让函数返回特定标签

1
2
3
4
def maketag(func):
def wrapper():
return "<h>" + func() + "</h>"
return wrapper

我们定义了一个函数maketag(),该函数有一个参数func,这个参数必须是一个函数,内部又定义了一个wrapper()函数,并直接返回给maketag()

1
2
3
>>> hello = maketag(hello)  # 将 hello 函数传给 maketag
>>> hello()
'<h>hello world</h>'

上面我们把最初hello函数传给maketag()函数并将返回赋值给变量hello,此时调用hello得到我们想要的结果。
注意: maketag()返回赋值给hello,hello函数本身还存在,但函数名引用己经变为了maketag返回函数的名称wrapper,不在是原来的hello,验证一下

1
2
3
4
5
6
7
In [6]: a = maketag(hello)
In [8]: a.__name__
Out[8]: 'wrapper'

In [9]: hello = maketag(hello)
In [10]: hello.__name__
Out[10]: 'wrapper'

总结一下:为了增强原函数hello的功能定义了一个maketag函数,它接收一个函数作为参数,maketag返回一个新的函数赋值给一个变量hello,然后hello可直接调用。

一般情况下,我们使用装饰器提供的 @ 语法糖(Syntactic Sugar)来简化上面的写法:

1
2
3
4
5
6
7
8
def maketag(func):
def wrapper():
return "<h>" + func() + "</h>"
return wrapper

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

像上面的情况,可以动态修改函数(或类)功能的函数就是装饰器。本质上,它是一个高阶函数,以被装饰的函数(比如上面的 hello)为参数,并返回一个包装后的函数(比如上面的 wrapper)给被装饰函数(hello)。

单/多个装饰器的使用形式

装饰器的一般使用形式如下:

1
2
3
4
5
6
7
8
@decorator
def func():
pass
等价于下面的形式:

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

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

1
2
3
4
5
6
7
8
9
10
@decorator_one
@decorator_two
def func():
pass
等价于:

def func():
pass

func = decorator_one(decorator_two(func))

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def maketag1(func):
def wrapper():
return '<html>' + func() + '</html>'
return wrapper

def maketag2(func):
def wrapper():
return '<i>' + func() + '</i>'
return wrapper

@maketag1
@maketag2
def hello():
return 'hello world'

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

1
2
3
4
5
6
7
8
def hello():
return 'hello world'

hello = maketag1(maketag2(hello))
调用函数 hello:

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

似乎理解深刻了:当多个装饰器时,离函数定义最近的装饰器先被调用

对带参数的函数进行装饰

让被装饰函数带有参数,对前面例子中的 hello() 函数进行改写使其带参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def maketag(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return '<h>' + res + '</h>'
return wrapper

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

@maketag
def hello2(name1='user1', name2='user2'):
return 'hello %s, %s' % (name1, name2)

由于函数 hello 带参数,因此内嵌包装函数 wrapper 也做了一点改变:

内嵌包装函数的参数传给了 func,即被装饰函数,也就是说内嵌包装函数的参数跟被装饰函数的参数对应,这里使用了 (*args, **kwargs),是为了适应可变参数。
看看使用:

1
2
3
4
>>> hello1('python')
'<h>hello python</h>'
>>> hello2('python', 'java')
'<h>hello python, java</h>'

带参数的装饰器

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

1
2
3
4
5
6
7
8
9
@decorator(args1, args2)
def func():
pass
等价于:

def func():
pass

func = decorator(args1, args2)(func)

我们想改用标签 <html>...</html> 是不是要再定义一个装饰器呢?不必,其实我们可以装饰器外层再定义一个函数,将标签作为参数,返回一个装饰器,比如:

1
2
3
4
5
6
7
8
def outer_decorator(tag):     # 外层函数参数传给装饰器
def decorator(func): # 真正的装饰器参数必然是被装饰器函数func
def wrapper(*args, **kwargs): # 装饰器内层函数的参数是func的参数
res = func(*args, **kwargs) # 被装饰函数正常调用
# return '<' + tag + '>' + res + '</' + tag + '>'
return '<{tag}>{res}</{tag}>'.format(tag=tag, res=res)
return wrapper # 内层函数作返回值返回给装饰器
return decorator # 装饰器本身作为返回值返回给最外层函数

现在,我们可以根据需要生成想要的装饰器了:

1
2
3
4
5
6
7
outer_decorator = outer_decorator('html')
@outer_decorator
def hello(name):
return 'hello %s' % name

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

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

1
2
3
@outer_decorator('html')     # @语法糖syntactic sugar
def hello(name):
return 'hello, %s' % name

这就是带参数的装饰器,其实就是在装饰器外面多了一层包装,根据不同的参数返回不同的装饰器。

基于类的装饰器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
class Deco_C(object):
def __init__(self, func):
self.func = func

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

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

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

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

__init__():它接收一个函数作为参数,也就是被装饰的函数
__call__():让类对象可调用,就像函数调用一样,在调用被装饰函数时被调用
还可以让类装饰器带参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Tag(object):
def __init__(self, tag):
self.tag = tag

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

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

>> hello(', welcome')
'<html>hello, welcome</html>'

需要注意的是,如果类装饰器有参数,则 init 接收此参数,而 call 接收 func函数,并多了一层wrapper(*args, **kwargs)来接收func函数的参数。

装饰器的副作用

前面提到装饰器有一个瑕疵,就是被装饰函数的函数名称已经不是原来的名称了,回到最开始的例子:

1
2
3
4
5
6
7
8
def maketagc(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper

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

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

1
2
>>> hello.__name__
'wrapper'

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from functools import wraps

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

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

>>> hello.__name__
'hello'

小结:
本质上,装饰器就是一个有返回函数的高阶函数。
装饰器可以动态地修改一个类或函数的功能,通过在原有的类或者函数上包裹一层修饰类或修饰函数实现。
事实上,装饰器就是闭包的一种应用,但它比较特别,以被装饰函数为参数,并返回一个函数,赋给被装饰函数,闭包则没这种限制。

装饰器生产化进阶

eg1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def deco(func):
def wrapper(x):
print("what ...")
func(x)
print('Game over ...')
return wrapper

@deco
def show(x):
print(x)
>>>show("I am a pythoner")
what ...
I am a pythoner
Game over ...

eg2: