第7章 函数装饰器与闭包
函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。
基础知识
装饰器的一大特性是, 能把被装饰的函数替换成其他函数。 第二个特性是, 装饰器
在加载模块时立即执行
例如:
def deco(func):
def inner():
print('running inner()')
return inner
# 等价于deco(target)
@deco
def target():
print('running target()')
if __name__ == '__main__':
print(target)
target()
输出:
<function deco.<locals>.inner at 0x00000179D67E4708>
running inner()
分析:
函数target
被装饰函数deco
替换成<function deco.<locals>.inner
装饰器的一个关键特性是, 它们在被装饰的函数定义之后立即运行。 这通常是在导入时
(即 Python 加载模块时)
例如:该文件文registration.py
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__ == '__main__':
main()
当该模块被导入时:import registration
,会运行装饰器函数register
。 很多 Python Web 框架使用这样的装饰器把函数添加到某种中央注册处, 例如把URL模式映射到生成 HTTP 响应的函数上的注册处
闭包
闭包指延伸了作用域的函数, 其中包含函数定义体中引用、 但是不在定义体中定义
的非全局变量。 函数是不是匿名的没有关系, 关键是它能访问定义体之外定义的非全局变
量。
例如:
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total / len(self.series)
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
if __name__ == '__main__':
avg = Averager()
print(avg(10))
print(avg(11))
print(avg(12))
avg1 = make_averager()
print(avg1)
print(avg1(10))
print(avg1(11))
print(avg1(12))
print(avg1.__code__.co_varnames)
print(avg1.__code__.co_freevars)
print(avg1.__closure__)
print(avg1.__closure__[0].cell_contents)
输出:
10.0
10.5
11.0
<function make_averager.<locals>.averager at 0x000001B72FB2E708>
10.0
10.5
11.0
('new_value', 'total')
('series',)
(<cell at 0x000001B72FCB6D38: list object at 0x000001B72DE35448>,)
[10, 11, 12]
Process finished with exit code 0
分析:
avg是个可调用对象,每次调用时将参数new_value
存放到self.series
,根据输出结果avg1
是个函数对象
<function make_averager.<locals>.averager
, 注意, series 是 make_averager 函数的局部变量, 因为那个函数的定义体中初始化了series: series = []。 可是, 调用 avg1(10) 时, make_averager 函数已经返回了,
而它的本地作用域也一去不复返了,它从哪里获取 series
,series
不是在 函数averager
的作用域外吗?
avg1.__code__.co_varnames
保存了变量名,而avg1.__code__.co_freevars
保存了自由变量,即make_averager
中series
,如下图所示:
series
的绑定在返回的 avg1 函数的 closure 属性中。 avg1.__closure__ 中
的各个元素对应于 avg1.__code__.co_freevars
中的一个名称。 这些元素是 cell
对象, 有个cell_contents
属性, 保存着真正的值 。
nonlocal声明
Python 3 引入了 nonlocal 声明。 它的作用是把变量标记为自由变量, 即使在函数中为变量赋予新值了, 也会变成自由变量。 如果为 nonlocal 声明的变量赋予新值, 闭包中保存的绑定会更新。
例子:
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
if __name__ == '__main__':
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))
print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
c = avg.__closure__
print(avg.__closure__[0].cell_contents)
pass
分析:
当 count 是数字或任何不可变类型时, count += 1 语句的作用其实与 count= count + 1 一样。 因此, 我们在 averager
的定义体中为 count 赋值了, 这会把count 变成局部变量。 total 变量也受这个问题影响。
但是对数字、 字符串、 元组等不可变类型来说, 只能读取, 不能更新。 如果尝试重新绑定, 例如 count = count + 1, 其实会隐式创建局部变量 count。 这样, count 就不是自由变量了, 因此不会保存在闭包中。
实现一个简单的装饰器
import time
import functools
import requests
# 一个计算函数运行时间的装饰器
def clock(func):
@functools.wraps(func)
def clocked(url, *args, **kwargs):
t0 = time.time()
result = func(url, *args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s)' % (elapsed, name, arg_str))
return result
return clocked
@clock
def download(url, *args, **kwargs):
return requests.get(url).content
if __name__ == '__main__':
download("https://home.firefoxchina.cn/", "arg1", "arg2", key1="value1")
使用functools.wraps
装饰器把相关的属性如__name__
和__doc__
从 func
复制到 clocked
中 ,而download
的参数会被复制到clocked
中。
综合应用
下面是django
一个编写视图函数的用法,通过叠放装饰器、参数化装饰器来让限定视图函数post_new
必须满足要登录、
是ajax请求且是POST请求
def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
"""
Decorator for views that checks that the user is logged in, redirecting
to the log-in page if necessary.
"""
actual_decorator = user_passes_test(
lambda u: u.is_authenticated,
login_url=login_url,
redirect_field_name=redirect_field_name
)
if function:
return actual_decorator(function)
return actual_decorator
def ajax_required(func):
"""验证是否为AJAX请求"""
@wraps(func)
def wrap(request, *args, **kwargs):
# request.is_ajax() 判断是否为ajax请求
if not request.is_ajax():
return HttpResponseBadRequest
return func(request, *args, **kwargs)
return wrap
def require_http_methods(request_method_list):
"""
Decorator to make a view only accept particular request methods. Usage::
@require_http_methods(["GET", "POST"])
def my_view(request):
# I can assume now that only GET or POST requests make it this far
# ...
Note that request methods should be in uppercase.
"""
def decorator(func):
@wraps(func)
def inner(request, *args, **kwargs):
if request.method not in request_method_list:
response = HttpResponseNotAllowed(request_method_list)
log_response(
'Method Not Allowed (%s): %s', request.method, request.path,
response=response,
request=request,
)
return response
return func(request, *args, **kwargs)
return inner
return decorator
@login_required
@ajax_required
@require_http_methods(["POST"])
def post_new(request):
"""发送动态, AJAX POST请求"""
pass