Python系列之装饰器(decorator)
Python装饰器的本质
Python的装饰器本质上是一个嵌套函数,它接受被装饰的函数(func)作为参数,并返回一个包装过的函数。这样我们可以在不改变被装饰函数的代码的情况下给被装饰函数或程序添加新的功能。Python的装饰器广泛应用于缓存、权限校验(如django中的@login_required和@permission_required装饰器)、性能测试(比如统计一段程序的运行时间)和插入日志等应用场景。有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码,增加一个函数的重用性。
试想你写了很多程序,一直运行也没啥问题。有一天老板突然让你统计每个程序都运行了多长时间并比较下运行效率。此时如果你去手动修改每个程序的代码一定会让你抓狂,而且还破坏了那些程序的重用性。聪明的程序员是绝不能干这种蠢事的。此时你可以编写一个@time_it的装饰器(代码如下所示)。如果你想打印出某个函数或程序运行时间,只需在函数前面@一下,是不是很帅?
import time
def time_it(func):
def inner():
start = time.time()
func()
end = time.time()
print('用时:{}秒'.format(end-start))
return inner
@time_it
def func1():
time.sleep(2)
print("Func1 is running.")
if __name__ == '__main__':
func1()
运行结果如下:
Func1 is running.
用时:2.0056326389312744
由于Python装饰器的工作原理主要依赖于嵌套函数和闭包,所以我们必须先对嵌套函数和闭包有深入的了解。嵌套函数和闭包几乎是Python工作面试必考题哦。
嵌套函数
如果在一个函数的内部还定义了另一个函数(注意: 是定义,不是引用!),这个函数就叫嵌套函数。外部的我们叫它外函数,内部的我们叫他内函数。
我们先来看一个最简单的嵌套函数的例子。我们在outer函数里又定义了一个inner函数,并调用了它。你注意到了吗? 内函数在自己作用域内查找局部变量失败后,会进一步向上一层作用域里查找。
def outer():
x = 1
def inner():
y = x + 1
print(y)
inner()
outer() #输出结果 2
如果我们在外函数里不直接调用内函数,而是通过return inner返回一个内函数的引用 这时会发生什么呢? 你将会得到一个内函数对象,而不是运行结果。
def outer():
x = 1
def inner():
y = x + 1
print(y)
return inner
outer() # 输出<function outer.<locals>.inner at 0x039248E8>
f1 = outer()
f1() # 输出2
上述这个案例比较简单,因为outer和inner函数都是没有参数的。我们现在对上述代码做点改动,加入参数。你可以看到外函数的参数或变量可以很容易传递到内函数。
def outer(x):
a = x
def inner(y):
b = y
print(a+b)
return inner
f1 = outer(1) # 返回inner函数对象
f1(10) # 相当于inner(10)。输出11
如果上例中外函数的变量x换成被装饰函数对象(func),内函数的变量y换成被装饰函数的参数,我们就可以得到一个通用的装饰器啦(如下所示)。你注意到了吗? 我们在没对func本身做任何修改的情况下,添加了其它功能, 从而实现了对函数的装饰。
def decorator(func):
def inner(*args, **kwargs):
add_other_actions()
return func(*args, **kwargs)
return inner
请你仔细再读读上面这段代码,我们的decorator返回的仅仅是inner函数吗? 答案是不。它返回的其实是个闭包(Closure)。整个装饰器的工作都依赖于Python的闭包原理。
闭包(Closure)
闭包是Python编程一个非常重要的概念。如果一个外函数中定义了一个内函数,且内函数体内引用到了体外的变量,这时外函数通过return返回内函数的引用时,会把定义时涉及到的外部引用变量和内函数打包成一个整体(闭包)返回。我们在看下之间案例。我们的outer方法返回的只是内函数对象吗? 错。我们的outer函数返回的实际上是一个由inner函数和外部引用变量(a)组成的闭包!
def outer(x):
a = x
def inner(y):
b = y
print(a+b)
return inner
f1 = outer(1) # 返回inner函数对象+局部变量1(闭包)
f1(10) # 相当于inner(10)。输出11
一般一个函数运行结束的时候,临时变量会被销毁。但是闭包是一个特别的情况。当外函数发现,自己的临时变量会在将来的内函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量同内函数绑定在一起。这样即使外函数已经结束了,内函数仍然能够使用外函数的临时变量。这就是闭包的强大之处。
如何编写一个通用的装饰器
我们现在可以开始动手写个名为hint的装饰器了,其作用是在某个函数运行前给我们提示。这里外函数以hint命名,内函数以常用的wrapper(包裹函数)命名。
def hint(func):
def wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
return func(*args, **kwargs)
return wrapper
@hint
def hello():
print("Hello!")
我们现在对hello已经进行了装饰,当我们调用hello()时,我们可以看到如下结果。
>>> hello()
hello is running.
Hello!
值得一提的是被装饰器装饰过的函数看上去名字没变,其实已经变了。当你运行hello()后,你会发现它的名字已经悄悄变成了wrapper,这显然不是我们想要的(如下图所示)。这一点也不奇怪,因为外函数返回的是由wrapper函数和其外部引用变量组成的闭包。
>>> hello.__name__
'wrapper'
为了解决这个问题保证装饰过的函数__name__属性不变,我们可以使用functools模块里的wraps方法,先对func变量进行wraps。下面这段代码可以作为编写一个通用装饰器的示范代码,注意收藏哦。
from functools import wraps
def hint(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
return func(*args, **kwargs)
return wrapper
@hint
def hello():
print("Hello!")
借助Python的decorator模块(需事先安装)可以简化装饰器的编写和使用。如下所示。
from decorator import decorator
@decorator
def hint(func, *args, **kwargs):
print('{} is running'.format(func.__name__))
return func(*args, **kwargs)
编写带参数的高级装饰器
前面几个装饰器一般是内外两层嵌套函数。如果我们需要编写的装饰器本身是带参数的,我们需要编写三层的嵌套函数,其中最外一层用来传递装饰器的参数。现在我们要对@hint装饰器做点改进,使其能通过@hint(coder=“John”)传递参数。该装饰器在函数运行前给出提示的时候还显示函数编写人员的名字。完整代码如下所示:
from functools import wraps
def hint(coder):
def wrapper(func):
@wraps(func)
def inner_wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
print('Coder: {}'.format(coder))
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@hint(coder="John")
def hello():
print("Hello!")
下面这段代码是一段经典的Python装饰器代码,显示了@cache这个装饰器怎么编写和工作的。它需要使用缓存实例做为一个参数,所以也是三层嵌套函数。
import time
from functools import wraps
# 装饰器增加缓存功能
def cache(instance):
def wrapper(func):
@wraps(func)
def inner_wrapper(*args, **kwargs):
# 构建key: key => func_name::args::kwargs
joint_args = ','.join((str(x) for x in args))
joint_kwargs = ','.join('{}={}'.format(k, v) for k, v in sorted(kwargs.items()))
key = '{}::{}::{}'.format(func.__name__,joint_args, joint_kwargs)
# 根据key获取结果。如果key已存在直接返回结果,不用重复计算。
result = instance.get(key)
if result is not None:
return result
# 如果结果不存在,重新计算,缓存。
result = func(*args, **kwargs)
instance.set(key, result)
return result
return inner_wrapper
return wrapper
# 创建字典构造函数,用户缓存K/V键值对
class DictCache:
def __init__(self):
self.cache = dict()
def get(self, key):
return self.cache.get(key)
def set(self, key, value):
self.cache[key] = value
def __str__(self):
return str(self.cache)
def __repr__(self):
return repr(self.cache)
# 创建缓存对象
cache_instance = DictCache()
# Python语法糖调用装饰器
@cache(cache_instance)
def long_time_func(x):
time.sleep(x)
return x
# 调用装饰过函数
long_time_func(3)
基于类实现的装饰器
Python的装饰器不仅可以用嵌套函数来编写,还可以使用类来编写。其调用__init__方法创建实例,传递参数,并调用__call__方法实现对被装饰函数功能的添加。
from functools import wraps
#类的装饰器写法, 不带参数
class Hint(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('{} is running'.format(self.func.__name__))
return self.func(*args, **kwargs)
#类的装饰器写法, 带参数
class Hint(object):
def __init__(self, coder=None):
self.coder = coder
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('{} is running'.format(func.__name__))
print('Coder: {}'.format(self.coder))
return func(*args, **kwargs) # 正式调用主要处理函数
return wrapper
常用装饰器应用场景及正确使用方法
权限验证
@login_required
@login_required是Django最常用的一个装饰器。其作用是在执行视图函数前先检查用户是否通过登录身份验证,并将未登录的用户重定向到指定的登录url。其中login_url是可选参数。如果不设置,默认login_url是settings.py里设置的LOGIN_URL。
from django.contrib.auth.decorators import login_required
@login_required(login_url='/accounts/login/')
def my_view(request):
...
@login_required还可以一个可选参数是redirect_field_name, 默认值是’next’。
from django.contrib.auth.decorators import login_required
@login_required(redirect_field_name='my_redirect_field')
def my_view(request):
...
注意:
login_required装饰器不会检查用户是否是is_active状态。如果你想进一步限制登录验证成功的用户对某些视图函数的访问,你需要使用更强大的@user_passes_test装饰器。
@user_passes_test
@user_passes_test装饰器的作用是对登录用户对象的信息进行判断,只有通过测试(返回值为True)的用户才能访问视图函数。不符合条件的用户会被跳转到指定的登录url。
@user_passes_test装饰器有一个必选参数,即对用户对象信息进行判断的函数。该函数必需接收user对象为参数。与@login_required类似,@user_passes_test还有两个可选参数(login_url和redirect_field_name),这里就不多讲了。
user_passes_test(func[,login_url=None, redirect_field_name=REDIRECT_FIELD_NAME])
下例中@user_passes_test装饰器对用户的email地址结尾进行判断,会把未通过测试的用户会定向到登录url。试想一个匿名用户来访问,她没有email地址,显然不能通过测试,登录后再来吧。
from django.contrib.auth.decorators import user_passes_test
def email_check(user):
return user.email.endswith('@example.com')
@user_passes_test(email_check)
def my_view(request):
...
如果需要加可选参数,可以按如下方式使用。
@user_passes_test(email_check, login_url='/login/'):
def my_view(request):
...
注意:
@user_passes_test不会自动的检查用户是否是匿名用户, 但是@user_passes_test装饰器还是可以起到两层校验的作用。一来检查用户是否登录,二来检查用户是否符合某些条件,无需重复使用@login_required装饰器。
我们如果只允许is_active的登录用户访问某些视图,我们现在可以使用@user_passes_test装饰器轻松地解决这个问题,如下所示:
from django.contrib.auth.decorators import user_passes_test@user_passes_test(lambda u: u.is_active)
def my_view(request):
...
@permission_required
@permission_required装饰器的作用是检查用户用户是否有特定权限,第一个参数perm是权限名,为必选, 第二个参数login_url为可选。
permission_required(perm[, login_url=None, raise_exception=False])
下例检查用户是否有polls.can_vote的权限,没有的话定向至login_url。如果你设置了raise_exception=True, 会直接返回403无权限的错误,而不会跳转到登录页面。那么问题来了,我们需要先使用@login_required来验证用户是否登录,再使用@permission_required装饰器来查看登录用户是否具有相关权限吗? 答案是不需要。如果一个匿名用户来访问这个视图,显然该用户没有相关权限,会自动定向至登录页面。
from django.contrib.auth.decorators import permission_required
@permission_required('polls.can_vote', login_url='/login/')
def my_view(request):
...
缓存
缓存是Django装饰器很重要的一个应用场景。下面我们来看几个主要的缓存装饰器。注意: 使用以下装饰器的前提是你已经对缓存进行了相关设置
@cache_page
该装饰器可以接收缓存的时间作为参数,比如下例缓存页面15分钟。
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def my_view(request):
...
@cache_control
通常用户将会面对两种缓存: 他或她自己的浏览器缓存(私有缓存)以及他或她的提供者缓存(公共缓存)。 公共缓存由多个用户使用,而受其它人的控制。 这就产生了你不想遇到的敏感数据的问题,比如说你的银行账号被存储在公众缓存中。 因此,Web 应用程序需要以某种方式告诉缓存那些数据是私有的,哪些是公共的。cache_control 装饰器可以解决这个问题。
from django.views.decorators.cache import cache_control
@cache_control(private=True)
def my_view(request):
# ...
该修饰器负责在后台发送相应的 HTTP 头部。还有一些其他方法可以控制缓存参数。 例如, HTTP 允许应用程序执行如下操作:
- 定义页面可以被缓存的最大时间。
- 指定某个缓存是否总是检查较新版本,仅当无更新时才传递所缓存内容。
在 Django 中,可使用 cache_control 视图修饰器指定这些缓存参数。 在下例中, cache_control 告诉缓存对每次访问都重新验证缓存并在最长 3600 秒内保存所缓存版本。
from django.views.decorators.cache import cache_control
@cache_control(must_revalidate=True, max_age=3600)
def my_view(request):
# ...
在 cache_control() 中,任何合法的Cache-Control HTTP 指令都是有效的。下面是完整列表:
- public=True
- private=True
- no_cache=True
- no_transform=True
- must_revalidate=True
- proxy_revalidate=True
- max_age=num_seconds
- s_maxage=num_seconds
@vary_on_headers
缺省情况下,Django 的缓存系统使用所请求的路径(如blog/article/1)来创建其缓存键。这意味着不同用户请求同样路径都会得到同样的缓存版本,不考虑客户端user-agent, cookie和语言配置的不同, 除非你使用Vary头部通知缓存机制需要考虑请求头里的cookie和语言的不同。
要在 Django 完成这项工作,可使用便利的 vary_on_headers 视图装饰器。例如下面代码告诉Django读取缓存数据时需要同时考虑User-Agent和Cookie的不同。与此类似的装饰器还有@vary_on_cookie。
from django.views.decorators.vary import vary_on_headers
@vary_on_headers('User-Agent', 'Cookie')
def my_view(request):
...
@never_cache
如果你想用头部完全禁掉缓存, 你可以使用@never_cache装饰器。如果你不在视图中使用缓存,服务器端是肯定不会缓存的,然而用户的客户端如浏览器还是会缓存一些数据,这时你可以使用never_cache禁用掉客户端的缓存。
from django.views.decorators.cache import never_cache
@never_cachedef myview(request):
# ...
其它常用装饰器
@method_decorator
前面的案例中,我们的装饰器都是直接使用在函数视图上的。如果需要在基于类的视图上使用装饰器,我们需要使用到@method_decorator这个装饰器, 它的作用是将类伪装成函数方法。@method_decorator第一个参数一般是需要使用的装饰器名。
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decoratorfrom django.views.generic import TemplateView
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'
@require_http_methods
该装饰器的作用是限制用户的请求方法。如下例中仅接收GET和POST方法。与此类似的装饰器还有@require_POST, @require_GET和@require_safe。
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def my_view(request):
# Only accept GET or POST method
pass
@gzip_page
该装饰器可以压缩内容,前提是用户客户端允许内容压缩的话。使用方法如下:
from django.views.decorators.gzip import gzip_page
@gzip_page
def my_view(request):
# Only accept GET or POST method
pass
使用多重装饰器(在sanic框架中,类中经常使用作登录验证)
你可以在一个函数或基于类的视图上使用多重装饰器,但一定要考虑装饰器执行的先后顺序。比如下例中会先执行@never_cache, 再执行@login_required。
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decoratorfrom django.views.decorators.cache import never_cache
@method_decorator(never_cache, name='dispatch')@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'class ProtectedView(TemplateView):
template_name = 'secret.html'
上例等同于:
decorators = [never_cache, login_required]
@method_decorator(decorators, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'