⽽实际⼯作中,装饰器通常运⽤在身份认证、⽇志记录、输⼊合理性检查等多个领域中。合理使⽤装饰器,往往能极⼤地提⾼程序的可读性以及运⾏效率。
所谓的装饰器,其实就是通过装饰器函数或者装饰器类,来修改原函数的功能,使得原函数不需要修改就具备新特性的机制。
1.一个简单的装饰器
我们可以先来看一个装饰器的简单例子:
import functools
def makebold(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return "<b>" + fn(*args, **kwargs) + "</b>"
return wrapper
@makebold
def greet(message):
return message
@makebold
def greet2():
return "hello"
print(greet.__name__)
print(greet('hello world'))
print(greet2())
上面代码的输出:
greet
<b>hello world</b>
<b>hello</b>
这段代码中,makehold就是一个装饰器函数,内部函数wrapper()中调⽤了原函数fn(*args, **kwargs),在原函数返回值的前后分别拼上了<b>
和</b>
,从而改变了原函数的行为。装饰器函数返回内部函数wrapper。
用@makebold
语法去修饰greet函数,这样原函数 greet() 不需要任何变化,在调用greet()时,就会在原来返回值message前后增加<b>
和</b>
字符串了。
这⾥的@被称之为语法糖,@makebold相当于前⾯的greet=makebold(greet)语句。因此,如果你的程序中有其它函数需要做类似的装饰,你只需在它们的上⽅加上@makebold就可以了,这样就⼤⼤提⾼了函数的重复利⽤和程序的可读性。
内部函数wrapper使⽤内置的装饰器@functools.wrap装饰,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷⻉到对应的装饰器函数⾥)。如果不用装饰器@functools.wrap,那么greet.__name__
的值将是wrapper。
2.装饰器的嵌套
Python支持对一个函数同时应用多个装饰器,多个装饰器,主要是要关注装饰器执行顺序问题:
import functools
def makebold(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return "<b>" + fn(*args, **kwargs) + "</b>"
return wrapper
def makeitalic(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return "<i>" + fn(*args, **kwargs) + "</i>"
return wrapper
@makebold
@makeitalic
def hello(s):
return s
@makeitalic
@makebold
def world(s):
return s
print(hello('hello'))
print(world('world'))
上面代码的输出:
<b><i>hello</i></b>
<i><b>world</b></i>
上面代码中,hello()和world()均被两个装饰器装饰了,只是装饰器的顺序不同。从代码的输出中看到,因为装饰器的顺序不同,输出也是不同的。
装饰器的执行顺序是从下往上。hello()函数装饰后等效于makebold(makeitalic(hello)),world()函数装饰后等效于函数装饰后等效于makeitalic(makebold(hello))。
3. 装饰器本身带参数
装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受⾃⼰定义的参数。举个例⼦,⽐如我想要定义⼀个参数,来表示装饰器内部函数被执⾏的次数,那么就可以写成下⾯
这种形式:
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(3)
def greet(message):
print(message)
greet('hello world')
上面代码输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
从输出上看,greet函数被执行了3次。仔细观察上面装饰器,是在原有不带参数装饰器函数外边又套了一层。
4. 类装饰器
类也可以作为装饰器。类装饰器主要依赖于函数__call_(),每当你调⽤⼀个类的示例时,函数__call__()就会被执⾏⼀次。看个简单的例子:
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
example()
example()
上面代码的输出:
num of calls is: 1
hello world
num of calls is: 2
hello world
Count类是一个装饰器类,初始化时传⼊原函数func(),⽽__call__()函数表示让变量num_calls ⾃增 1,然后打印,并且调⽤原函数。因此,在我们第⼀次调⽤函数 example() 时,num_calls 的值是 1,⽽在第⼆次调⽤时,它的值变成了2。
再举一个例子:
class Foo(object):
def __init__(self):
pass
def __call__(self, func):
def _call(*args, **kw):
print('class decorator runing')
return func(*args, **kw)
return _call
class Bar(object):
@Foo()
def bar(self, test, ids):
print(test, ids)
Bar().bar('aa', 'ids')
我们经常会用到的比如说给多线程函数加锁来保证共享数据操作的完整性。线程锁装饰器如下:
import threading
class StandardOut(object):
"""
线程锁装饰器
"""
def __init__(self):
self.thread_lock = threading.Lock()
def __call__(self,func):
def _call(*args,**kw):
self.thread_lock.acquire()
func(*args,**kw)
self.thread_lock.release()
return _call
5.装饰器的实际应用
5.1 身份认证
⾸先是最常⻅的身份认证的应⽤。⽐如⼀些⽹站,你不登录也可以浏览内容,但如果你想要发布⽂章或留⾔,在点击发布时,服务器端便会查询你是否登录。如果没有登录,就不允许这项操作。
我们来看一个代码示例:
import functools
def check_user_logged_in(request):
"""
判断用户是否处于登录状态。
"""
pass
def authenticate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if check_user_logged_in(args[0]): # 如果用户处于登录状态
return func(*args, **kwargs) # 执行函数 post_comment()
else:
raise Exception('Authentication failed')
return wrapper
@authenticate
def post_comment(request):
pass
authenticate是一个装饰器,在内部函数wrapper中,判断用户是否处于登录状态,如果是则执行被装饰函数func,否则抛出异常"Authentication failed"。
post_comment()是一个发表评论的函数,使用authenticate对其进行装饰,每次调⽤这个函数前,都会先检查⽤户是否处于登录状态,如果是登录状态,则允许这项操作;如果没有登录,则不允许。
5.2 ⽇志记录
⽇志记录同样是很常⻅的⼀个案例。在实际⼯作中,想在测试某些函数的执⾏时间,那么,装饰器就是⼀种很常⽤的⼿段。
我们通常⽤下⾯的⽅法来表示:
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
return res
return wrapper
@log_execution_time
def calculate_similarity():
time.sleep(3)
calculate_similarity()
上面代码输出:
calculate_similarity took 3001.337694 ms
这⾥,装饰器 log_execution_time 记录某个函数的运⾏时间,并返回其执⾏结果。如果你想计算任何函数的执⾏时间,在这个函数上⽅加上@log_execution_time即可。
5.3 参数的合理性检查
一些通用的参数检查可以放到一个装饰器函数中,然后使用装饰器去装饰需要做参数检查的函数:
import functools
def validation_check(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if all([isinstance(i, str) for i in args]):
return func(*args, **kwargs)
else:
raise Exception('位置参数不全是擦字符串类型')
return wrapper
@validation_check
def neural_network_training(param1, param2):
print(param1, param2)
neural_network_training("123", "a") # 输出123 a
neural_network_training("123", 123) # 输出异常
这个例子是对neural_network_training函数的参数做检查,当位置参数全是字符串类型是才执行,否则不执行。
总结
当我们希望原始函数保持不变的情况下,对其增加新的特性时,就可以使用装饰器。装饰器的应用范围很广,比如身份认证、⽇志记录、输⼊合理性检查、路由等多个场景。
装饰器可以有两种类型,一种是函数装饰器,一种是类装饰器,写成类的话,优点是程序的分解度更加高,具体用类装饰器和函数装饰器,视情况而定,二者本质是一样的。