APP下载
报价宝  ›  科技  › 

python中强大的装饰器

报价宝 来源:baojiabao.com 发布时间:2019-08-15 03:47:00 11月07日更新
报价宝综合消息python中强大的装饰器

装饰器一直以来都是Python中很有用、很经典的一个feature,在工程中的应用也十分广泛,比如日志、快取等等的任务都会用到。然而,在平常工作生活中,我发现不少人,尤其是初学者,常常因为其相对复杂的表示,对装饰器望而生畏,认为它“too fancy to learn”,实际并不如此。

函式->装饰器

函式核心回顾

引入装饰器之前,我们首先一起来复习一下,必须掌握的函式的几个核心概念。

第一点,我们要知道,在Python中,函式是一等公民(first-class citizen),函式也是物件。我们可以把函式赋予变数,比如下面这段程式码:

def func(message):

print('Got a message: {}'.format(message))

send_message = func

send_message('hello world')

# 输出

Got a message: hello world

这个例子中,我们把函式func()赋予了变数send_message,这样之后你呼叫send_message,就相当于是呼叫函式func()。

第二点,我们可以把函式当作引数,传入另一个函式中,比如下面这段程式码:

def get_message(message):

return 'Got a message: ' + message

def root_call(func, message):

print(func(message))

root_call(get_message, 'hello world')

# 输出

Got a message: hello world

这个例子中,我们就把函式get_message()以引数的形式,传入了函式root_call()中然后呼叫它。

第三点,我们可以在函式里定义函式,也就是函式的巢状。这里我同样举了一个例子:

def func(message):

def get_message(message):

print('Got a message: {}'.format(message))

return get_message(message)

func('hello world')

# 输出

Got a message: hello world

这段程式码中,我们在函式func()里又定义了新的函式get_message(),呼叫后作为func()的返回值返回。

第四点,要知道,函式的返回值也可以是函式物件(闭包),比如下面这个例子:

def func_closure():

def get_message(message):

print('Got a message: {}'.format(message))

return get_message

send_message = func_closure()

send_message('hello world')

# 输出

Got a message: hello world

这里,函式func_closure()的返回值是函式物件get_message()本身,之后,我们将其赋予变数send_message,再呼叫send_message(‘hello world’),最后输出了'Got a message: hello world'。

简单的装饰器

简单的复习之后,我们接下来学习今天的新知识——装饰器。按照习惯,我们可以先来看一个装饰器的简单例子:

def my_decorator(func):

def wrapper():

print('wrapper of decorator')

func()

return wrapper

def greet():

print('hello world')

greet = my_decorator(greet)

greet()

# 输出

wrapper of decorator

hello world

这段程式码中,变数greet指向了内部函式wrapper(),而内部函式wrapper()中又会呼叫原函式greet(),因此,最后呼叫greet()时,就会先打印'wrapper of decorator',然后输出'hello world'。

这里的函式my_decorator()就是一个装饰器,它把真正需要执行的函式greet()包裹在其中,并且改变了它的行为,但是原函式greet()不变。

事实上,上述程式码在Python中有更简单、更优雅的表示:

def my_decorator(func):

def wrapper():

print('wrapper of decorator')

func()

return wrapper

@my_decorator

def greet():

print('hello world')

greet()

这里的@,我们称之为语法糖,@my_decorator就相当于前面的greet=my_decorator(greet)语句,只不过更加简洁。因此,如果你的程式中有其它函式需要做类似的装饰,你只需在它们的上方加上@decorator就可以了,这样就大大提高了函式的重复利用和程式的可读性。

带有引数的装饰器

你或许会想到,如果原函式greet()中,有引数需要传递给装饰器怎么办?

一个简单的办法,是可以在对应的装饰器函式wrapper()上,加上相应的引数,比如:

def my_decorator(func):

def wrapper(message):

print('wrapper of decorator')

func(message)

return wrapper

@my_decorator

def greet(message):

print(message)

greet('hello world')

# 输出

wrapper of decorator

hello world

不过,新的问题来了。如果我另外还有一个函式,也需要使用my_decorator()装饰器,但是这个新的函式有两个引数,又该怎么办呢?比如:

@my_decorator

def celebrate(name, message):

...

事实上,通常情况下,我们会把*args和**kwargs,作为装饰器内部函式wrapper()的引数。*args和**kwargs,表示接受任意数量和型别的引数,因此装饰器就可以写成下面的形式:

def my_decorator(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')

# 输出:

wrapper of decorator

hello world

wrapper of decorator

hello world

wrapper of decorator

hello world

wrapper of decorator

hello world

原函式还是原函式吗?

现在,我们再来看个有趣的现象。还是之前的例子,我们试着打印出greet()函式的一些元资讯:

greet.__name__

## 输出

'wrapper'

help(greet)

# 输出

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

你会发现,greet()函式被装饰以后,它的元资讯变了。元资讯告诉我们“它不再是以前的那个greet()函式,而是被wrapper()函式取代了”。

为了解决这个问题,我们通常使用内建的装饰器@functools.wrap,它会帮助保留原函式的元资讯(也就是将原函式的元资讯,拷贝到对应的装饰器函式里)。

import functools

def my_decorator(func):

@functools.wraps(func)

def wrapper(*args, **kwargs):

print('wrapper of decorator')

func(*args, **kwargs)

return wrapper

@my_decorator

def greet(message):

print(message)

greet.__name__

# 输出

'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 calls is: {}'.format(self.num_calls))

return self.func(*args, **kwargs)

```

@Count

def example():

print("hello world")

example()

# 输出

num of calls is: 1

hello world

example()

# 输出

num of calls is: 2

hello world

...

这里,我们定义了类Count,初始化时传入原函式func(),而__call__()函式表示让变数num_calls自增1,然后打印,并且呼叫原函式。因此,在我们第一次呼叫函式example()时,num_calls的值是1,而在第二次呼叫时,它的值变成了2。

装饰器的巢状

回顾刚刚讲的例子,基本都是一个装饰器的情况,但实际上,Python也支援多个装饰器,比如写成下面这样的形式:

@decorator1

@decorator2

@decorator3

def func():

...

它的执行顺序从里到外,所以上面的语句也等效于下面这行程式码:

decorator1(decorator2(decorator3(func)))

这样,'hello world'这个例子,就可以改写成下面这样:

import functools

def my_decorator1(func):

@functools.wraps(func)

def wrapper(*args, **kwargs):

print('execute decorator1')

func(*args, **kwargs)

return wrapper

def my_decorator2(func):

@functools.wraps(func)

def wrapper(*args, **kwargs):

print('execute decorator2')

func(*args, **kwargs)

return wrapper

@my_decorator1

@my_decorator2

def greet(message):

print(message)

greet('hello world')

# 输出

execute decorator1

execute decorator2

hello world

装饰器用法例项

到此,装饰器的基本概念及用法我就讲完了,接下来,我将结合实际工作中的几个例子,带你加深对它的理解。

身份认证

首先是最常见的身份认证的应用。这个很容易理解,举个最常见的例子,你登入微信,需要输入使用者名称密码,然后点选确认,这样,服务器端便会查询你的使用者名称是否存在、是否和密码匹配等等。如果认证通过,你就可以顺利登入;如果不通过,就丢掷异常并提示你登入失败。

再比如一些网站,你不登入也可以浏览内容,但如果你想要释出文章或留言,在点击发布时,服务器端便会查询你是否登入。如果没有登入,就不允许这项操作等等。

我们来看一个大概的程式码示例:

import functools

def authenticate(func):

@functools.wraps(func)

def wrapper(*args, **kwargs):

request = args[0]

if check_user_logged_in(request): # 如果使用者处于登入状态

return func(*args, **kwargs) # 执行函式post_comment()

else:

raise Exception('Authentication failed')

return wrapper

@authenticate

def post_comment(request, ...)

...

这段程式码中,我们定义了装饰器authenticate;而函式post_comment(),则表示发表使用者对某篇文章的评论。每次呼叫这个函式前,都会先检查使用者是否处于登入状态,如果是登入状态,则允许这项操作;如果没有登入,则不允许。

日志记录

日志记录同样是很常见的一个案例。在实际工作中,如果你怀疑某些函式的耗时过长,导致整个系统的latency(延迟)增加,所以想在线上测试某些函式的执行时间,那么,装饰器就是一种很常用的手段。

我们通常用下面的方法来表示:

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(items):

...

这里,装饰器log_execution_time记录某个函式的执行时间,并返回其执行结果。如果你想计算任何函式的执行时间,在这个函式上方加上@log_execution_time即可。

输入合理性检查

再来看今天要讲的第三个应用,输入合理性检查。

在大型公司的机器学习框架中,我们呼叫机器丛集进行模型训练前,往往会用装饰器对其输入(往往是很长的json档案)进行合理性检查。这样就可以大大避免,输入不正确对机器造成的巨大开销。

它的写法往往是下面的格式:

import functools

def validation_check(input):

@functools.wraps(func)

def wrapper(*args, **kwargs):

... # 检查输入是否合法

@validation_check

def neural_network_training(param1, param2, ...):

...

其实在工作中,很多情况下都会出现输入不合理的现象。因为我们呼叫的训练模型往往很复杂,输入的档案有成千上万行,很多时候确实也很难发现。

试想一下,如果没有输入的合理性检查,很容易出现“模型训练了好几个小时后,系统却报错说输入的一个引数不对,成果付之一炬”的现象。这样的“惨案”,大大减缓了开发效率,也对机器资源造成了巨大浪费。

快取

最后,我们来看快取方面的应用。关于快取装饰器的用法,其实十分常见,这里我以Python内建的LRU cache为例来说明(如果你不了解 LRU cache,可以点选连结自行查阅)。

LRU cache,在Python中的表示形式是@lru_cache。@lru_cache会快取程序中的函式引数和结果,当快取满了以后,会删除least recenly used 的资料。

正确使用快取装饰器,往往能极大地提高程式执行效率。为什么呢?我举一个常见的例子来说明。

大型公司服务器端的程式码中往往存在很多关于装置的检查,比如你使用的装置是安卓还是iPhone,版本号是多少。这其中的一个原因,就是一些新的feature,往往只在某些特定的手机系统或版本上才有(比如Android v200+)。

这样一来,我们通常使用快取装饰器,来包裹这些检查函式,避免其被反复呼叫,进而提高程式执行效率,比如写成下面这样:

@lru_cache

def check(param1, param2, ...) # 检查使用者装置型别,版本号等等

...

总结

所谓的装饰器,其实就是通过装饰器函式,来修改原函式的一些功能,使得原函式不需要修改。

Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function.

而实际工作中,装饰器通常运用在身份认证、日志记录、输入合理性检查以及快取等多个领域中。合理使用装饰器,往往能极大地提高程式的可读性以及执行效率。

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 手机价格 汽车价格 笔记本价格 华为手机价格 耳机价格