装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
1.简单装饰器
为了更好的说明装饰器是什么,我们先来看一个简单例子,虽然实际代码可能比这复杂很多:
def f1():
print('This is a f1')
有一天,当我发现f1
函数不能满足现在的业务需要,需要在函数执行前打印一句话,于是在代码中添加日志代码:
def f1():
print('start running...')
print('This is a f1')
上面代码是实现了需求,但有一个问题,假如存在100个函数,不可能在100个函数中都添加一句打印的代码:
def print_info(func):
print('start running...')
func()
def f1():
print('This is a f1')
>>> print_info(f1)
'start runnin...'
'This is a f1'
这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 f1
函数,而是换成了 print_info
函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个f1
函数作为参数传递给print_info
函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。
def print_info(func):
def wrapper():
print('start running...')
func()
return wrappe
def f1():
print('This is a f1')
>>> f = print_info(f1)
>>> f()
'start runnin...'
'This is a f1'
print_info
就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func
包裹在其中,看起来像 f1
被 print_info
装饰了一样,print_info
返回的也是一个函数,这个函数的名字叫 wrapper
。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程
。
2.语法糖
如果你接触 Python 有一段时间了的话,想必你对 @
符号一定不陌生了,没错 @
符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。
def print_info(func):
def wrapper():
print('start running...')
func()
return wrappe
@print_info
def f1():
print('This is a f1')
>>> f1()
'start runnin...'
'This is a f1'
如上所示,有了@
,我们就可以省去f = print_info(f1)
这一句了,直接调用f1
即可得到想要的结果。你们看到了没有f1
函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
3.逻辑业务参数
可能有人问,如果我的业务逻辑函数f1
需要参数怎么办?比如:
def f1(name):
print('This is a' + name)
为实现传参,我们需要修改装饰器:
def print_info(func):
def wrapper(name):
print('start running...')
func(name)
return wrappe
@print_info
def f1(name):
print('This is a ' + name)
>>> f1('test')
'start running...'
'This is a test'
传入可变参数
def print_info(func):
def wrapper(*args):
print('start running...')
func(*args)
return wrappe
@print_info
def f1(name):
print('This is a ' + name)
@print_info
def f2(name,age):
print('This is a %s,age is %d' %(name, age))
>>> f1('test')
'start runnin...'
'This is a test'
>>> f2('f2',20)
'start runnin...'
'This is a f2,age is 20'
关键字参数
def print_info(func):
def wrapper(*args, **kw):
# args是一个数组,kw一个字典
print('start running...')
func(*args, **kw)
return wrappe
@print_info
def f3(name,age, **kw):
print('This is a %s,age is %d' %(name,age))
print(kw)
>>> f3('test',20, a=1,b=2,c='fwqw')
'start runnin...'
'This is a test,age is 20'
{'a': 1, 'b': 2, 'c': 'fwqw'}
4.带参数的装饰器
装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 f1
。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。
def print_info(level):
def decorator(func):
def wrapper(*args, **kw):
if level == 'warn':
print('warn is running...')
elif level == 'info':
print('info is running...')
func(*args,**kw)
return wrapper
return decorator
@print_info(level = 'warn')
def f1(name):
print('This is a %s' %(name))
f1('test')
'warn is running...'
'This is a test'
上面的print_info
是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@print_info(level="warn")
调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。
@print_info(level="warn")
等价于@decorator
5.类装饰器
没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__
方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
print ('class decorator runing')
self._func()
print ('class decorator ending')
@Foo
def bar():
print ('bar')
bar()
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring
、__name__
、参数列表,先看例子:
# 装饰器
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'with_logging'
print func.__doc__ # 输出 None
return func(*args, **kwargs)
return with_logging
# 函数
@logged
def f(x):
"""does some math"""
return x + x * x
logged(f)
不难发现,函数 f 被with_logging
取代了,当然它的docstring
,__name__
就是变成了with_logging
函数的信息了。好在我们有functools.wraps
,wraps
本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func
函数中,这使得装饰器里面的 func
函数也有和原函数 foo 一样的元信息了。
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'f'
print func.__doc__ # 输出 'does some math'
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
6.装饰器顺序
一个函数还可以同时定义多个装饰器,比如:
它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于
7.装饰器的副作用
当我们代码中使用装饰器之后会对原来的代码有什么影响:
def decorator(func):
def wrapper(*args, **kw):
print('start running...')
func(*args, **kw)
return wrapper
@decorator
def f1():
print(f1.__name__)
def f2():
print(f2.__name__)
>>> f2()
f2
>>> f1()
wrapper
这里小菜写了2个函数f1
和f2
主要是用做对比,f1是添加了装饰器,f2没有添加装饰器。从输出的结果看到,f2输出了自己函数名称f2
,添加了装饰器的f1函数输出了wrapper
,不在是自己的函数名f1
。
def decorator(func):
def wrapper(*args, **kw):
'''
This is wrapper
'''
print('start running...')
func(*args, **kw)
return wrapper
@decorator
def f1():
'''
This is a f1
'''
print(f1.__name__)
def f2():
'''
This is a f2
'''
print(f2.__name__)
>>> print(help(f2))
f2()
This is a f2
>>> print(help(f1))
wrapper(*args, **kw)
This is wrapper
一般出现这种情况好像没什么大问题,但是当我们需要查看函数说明文档时,就比较麻烦了,我想查看f1
函数说明,结果返回装饰器的wrappe
函数说明。那么如何修改呢?
+ from functools import wraps
def decorator(func):
+ @wraps(func)
def wrapper(*args, **kw):
'''
This is wrapper
'''
print('start running...')
func(*args, **kw)
return wrapper
@decorator
def f1():
'''
This is a f1
'''
print(f1.__name__)
def f2():
'''
This is a f2
'''
print(f2.__name__)
>>> print(help(f2))
f2()
This is a f2
>>> print(helf(f1))
+ f1()
+ This is a f1