Python核心技术与实战——十四|Python中装饰器的使用
我在以前的帖子里讲了装饰器的用法,这里我们来具体讲一讲Python中的装饰器,这里,我们从前面讲的函数,闭包为切入点,引出装饰器的概念、表达和基本使用方法。其次,我们结合一些实际工程中的例子,以便能再次理解。
一.函数与装饰器
函数的核心
第一点,在Python中,函数是“一等公民”(first-class citizen)(在有些资料例如流畅的Python中被叫做一等对象),函数也是对象,我们可以把函数赋予变量,比如下面的代码
def fun(message): print('Get a message:{}'.format(message)) send_message = fun send_message('hello world!') #输出 Get a message:hello world!
在这个例子中,我们把函数fun()赋给了变量send_message,这样之后调用send_message,就相当于调用了fun()
第二,我们还可以把函数当参数,传入另一个函数中,比如下面这段代码
def get_message(message): return 'get a message:'+message def root_call(func,message): print(func(message)) root_call(get_message,'hello world!')
这个例子中,我们就把函数get_message()以参数的形式传递给root_call()中然后调用他。
第三点,我们可以在函数中定义函数,也就是函数的嵌套。看看下面的例子:
def fun(message): def get_message(message): print('get a message:{}'.format(message)) return get_message(message) fun('hello world')
在这段代码中,我们在函数fun()中定义了新的函数get_message(),调用后fun的值返回。
第四点,函数的返回值也可以是函数对象(闭包),比如下面的例子
def fun_closure(): def get_message(message): print('got a message:{}'.format(message)) return get_message send_message = fun_closure() send_message('hello world')
在上面的例子里,函数func_closure()的返回值是get_message(),之后我们将其赋值给变量send_message,在调用该函数,最后输出字符串。
简单的装饰器
简单的复习了上面的知识点后,我们接下来看一看下面简单的装饰器
def my_decortor(fun): def wrapper(): print('wrapper of decorator') fun() return wrapper def greet(): print('hello world') greet = my_decortor(greet) greet()
在这段代码中,变量greet指向了内部函数wrapper(),而内部函数wrapper()有会调用原函数greet(),所以在调用greet()时,打印的字符串是会变化的。
这里的函数my_decortor()就是一个装饰器,他把需要执行的函数greet()包裹在其中,并且改变了他的行为,但是原函数不变。而上面的代码在Python中是可以有更加优雅的语法表示的
def my_decortor(fun): def wrapper(): print('wrapper of decorator') fun() return wrapper @my_decortor def greet(): print('hello world') greet()
这里的@,我们称之为语法糖,@my_decortor和前面的greet = my_decortor(greet)是等价的,只不过显得更加简洁。因此,程序中需要有其他的函数需要做类似的装饰,只需在他们上方添加@my_decorator就可以了。
带有参数的装饰器
还是上面的函数,可是如果我们的原函数greet()有参数需要传递给装饰器又该怎么办呢?
一个简单的方法就是在对应的装饰器上加上相应的参数
def my_decortor(func): def wrapper(message): #这里就把参数传递给了func print('wrapper of decorator') func(message) #func接收到参数 return wrapper @my_decortor def greet(message): print(message) greet('hello world')
可是新的问题又出来了!如果这里有另外一个函数,也需要my_decortor()这个装饰器,但是这个新的函数带了两个参数,又该怎么办呢?
@my_decortor def celebrate(name,message): pass
事实上,通常情况我们会把*args和**kwargs作为装饰器内部wrapper()函数的参数,因为这两个参数表示接受任意数量和类型的参数,所以装饰器内就可以写成这样的形式
def my_decortor(func): def wrapper(*args,**kwargs): print('wrapper of decorator') func(*args,**kwargs) return wrapper
带有自定义参数的装饰器
其实,装饰器还有更大程度的灵活性,上面的例子说明装饰器可以接受原函数任意类型和数量的参数。此外,他还可以自己接受自己定义的参数
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(4) #调用装饰器时传递参数 def greet(message): print(message) greet('hello world')
原函数韩式原函数么?
现在我们看下这个现象,还是之前的例子,我们打印一下greet()函数的一些元信息
print(greet.__name__)
看下结果,会发现greet被装饰器装饰以后,他的元信息发生变化,元信息告诉我们被装饰后的greet()已经被新的wrapper()取代了。
为了解决这个问题(有些时候我们需要保留原函数的元信息),我们通常使用内置装饰器@functools.wrap,他会帮助保留原函数的元信息(其实是把原函数的元信息拷贝到对应的装饰器函数里)。
import functools def my_decortor(func): @functools.wraps(func) def wrapper(*args,**kwargs): print('wrapper of decorator') func(*args,**kwargs) return wrapper def greet(message): print(message) print(greet.__name__) print(help(greet))
可以看到打印输出的函数名是greet()
类装饰器
前面讲的都是函数作为装饰器的用法,实际上类也可以作为装饰器。类装饰器主要依赖于函数__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 call is:{}'.format(self.num_calls)) return self.func(*args,**kwargs) @Count def example(): print('hello world') example() example()
这里,我们定义了一个类Count,初始化时候传入了原函数func(),而__call__()表示让变量num_calls自增1,然后打印。因此我们第一次调用example()时,打印的值为1,第二次就成2了。(其实这种用法我还不太清楚)。
装饰器的嵌套
前面的例子中,基本上都是一个装饰器的情况,但实际上,Python也支持多个装饰器,比方写成下面的形式
@deccorator1 @deccorator2 @deccorator2 def fun(): pass
他的执行顺序是从里到外,所以上面的语句也等价于下面的代码
deccorator1(deccorator2(deccorator3(func)))
放个例子吧,虽然这种用法可能我暂时用不到
import functools def my_decorator1(func): @functools.wraps(func) def wrapper(*args,**kwargs): print('excute decorator1') func(*args,**kwargs) return wrapper def my_decorator2(func): @functools.wraps(func) def wrapper(*args,**kwargs): print('excute decorator2') func(*args,**kwargs) return wrapper def my_decorator3(func): @functools.wraps(func) def wrapper(*args,**kwargs): print('excute decorator3') func(*args,**kwargs) return wrapper @my_decorator1 @my_decorator2 @my_decorator3 def greet(message): print(message)
装饰器用法实例
前面基本讲了装饰器所有的用法和概念,接下了我们就结合几个例子来加深对他的理解
身份认证
首先,最常见的使用环境是身份认证的应用,这个还是比较容易理解,比方我们登录微信,需要输入账户密码,然后点击确认,这样,服务器变回查询用户名是否存在,密码是否匹配,如果认证通过就可以顺利登录,如果不通过就抛出异常并提示登录失败。
而一些其他的网站,比如博客园,不登陆时也可以浏览内容,但如果想要发布文章或留言,在请求相关服务时,服务器端会查询是否登录,如果没有登录就不允许这项操作。
我们看看一个大概的代码示例
import functools def authenticate(func): @functools.wraps(func) def wrapper(*args,**kwargs): request = args[0] if check_user_logger_in(request): return func(*args,**kwargs) else: raise Exception('Authentication failed') return wrapper @authenticate def post_comment(request,...): pass
在上面的代码里,我们定义了装饰器authenticate,而函数post_commen()则表示发表用户对某篇文章的评论,每次调用这个函数前,都会先检查用户的登录状态,如果登录则允许操作,否则抛出登录异常的状态。
日志记录
日志记录也是个常见的案例,在实际工作中,如果怀疑某些函数耗时过长,导致整个系统的延迟(latency)增加,所以想在线测试某些函数的执行时间,那么装饰器就是很常用的手段
import time import functools def log_excution_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_excution_time def calulate_similarity(): print('func start') time.sleep(1) print('func stop') calulate_similarity()
这里,装饰器log_execution_time记录了某个函数的运行时间,并返回其结果。如果想计算任何函数的执行时间,在这个函数上加上@log_excution_time即可。
输入合理性检查
第三个应用,输入性检查。
在大型公司的机器学习框架中,我们调用机器集群进行模型训练前,往往会用装饰器对齐输入(往往是很长的json文件)进行合理性检查。这样就可以大大避免输入不正确对机器的巨大开销。他往往写成下面的格式:
import functools def validation_check(input): @functools.wraps(func) def wrapper(*args,**kwargs): ##检查输入是否合法 @validation_check def neural_network_training(parm1,parm2,...): pass
其实在工作中,很多情况下都会出现输入不合理的现象,因为我们调用的训练模型往往很复杂,输入的文件有成千上万行,很多时候确实难发现错误的地方。
试想一下,如果没有输入的合理性检查,很容易出现模型训练了好几个小时后系统报错说有个输入的参数不对,成果付之一炬。通过这样的方法大大减缓了开发效率,也避免对机器造成了巨大的浪费。
缓存
最后,装饰器还在缓存方面的使用也是十分常见的,Python内置的LRU cache就是最好的例子(LRU cache的连接可以自己点开查查)
LRU cache,在Python中的表示形式是@lru_cache。它会缓存进程中的函数和参数和结果,但缓存满了以后,会删除least recenly used的数据。
正确谁用缓存装饰器,往往能极大提高程序的运行效率。比方下面的例子:
大型公司的服务器端的代码往往存在很多关于设备的检查,比如使用的设备是安卓还是IOS,版本号是多少。这其中的一个原因是一些新的feature往往只能在某些特定的手机系统或版本上才有。
这样一来,我们通常使用缓存装饰器来包裹这些检查函数,避免其被反复调用,进而提高程序运行效率,比如写成这样
@lru_cache def check(param1,param2...): pass
总结
总之,装饰器的概念和用法,就是所谓装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改
而实际工作中,装饰器通常运用在身份认证,日志记录,输入合理性检查及缓存等多个领域中。合适的使用装饰器,往往能极大的提高程序的可读性及运行效率。