函数装饰器和闭包 补充知识点
- 缺少装饰器的基本语法: https://www.runoob.com/w3cnote/python-func-decorators.html
- 《设计模式:可复用面向对象软件的基础》 一书是这样概述“装饰器”模式的:“动态地给一个对象添加一些额 外的职责。”函数装饰器符合这一说法。
- LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
- 使用functools.lru_cache本质就是入参直接到结果的cache, 仅适用于静态计算资源的接口,动态计算或者认证不可使用。
- Python中list与collections.abc.Sequence是什么关系? https://www.zhihu.com/question/39383097
- 最后本书的审核和作者也提出,装饰器最好通过实现 call 方法的类实现,不应该像本章的示例那样通过函数实现。
其他
- 转换命令: jupyter nbconvert --to markdown E:PycharmProjectsTianChiProject 0_山枫叶纷飞competitions 13_fluent_pythonCH.07_函数_函数装饰器和闭包.ipynb
第七章学习目标
本章的最终目标
解释清楚函数装饰器的工作原理,包括最简单的注册装饰器和较为复杂的参数化装饰器。
但是,在实现这一目标之前,我们要讨论下述话题:
- Python 如何计算装饰器句法
- Python 如何判断变量是不是局部的
- 闭包存在的原因和工作原理
- nonlocal 能解决什么问题 (nonlocal是一个关于闭包和函数装饰器的关键字)
掌握这些基础知识后,我们可以进一步探讨装饰器:
- 实现行为良好的装饰器
- 标准库中有用的装饰器
- 实现一个参数化装饰器
7.1 装饰器基础知识
-
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。
-
装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个 函数或可调用对象。
-
Python 也支持类装饰器。
-
装饰器只是语法糖。如前所示,装饰器可以像常规的可调用 对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做 元编程(在运行时改变程序的行为)时。
-
综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。
示例有个名为decorate的装饰器:
def decorate(fun):
print('running target(decorate)')
@decorate
def target():
print('running target!')
running target(decorate)
由上可以知道,
装饰器是 decorate(target) 返回的函数。
示例 7-1 装饰器通常把函数替换成另一个函数
print('使用 deco 装饰 target')
def deco(func):
def inner():
print(' running inner() !')
return inner
@deco
def target():
print('running target()')
print('调用被装饰的 target 其实会运行 inner: ')
target()
running inner() !
print(' 审查对象,发现 target 现在是 inner 的引用: ')
target
<function __main__.deco.<locals>.inner()>
7.2 Python何时执行装饰器
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时),如示例 7-2 中的 registration.py 模块所示。
示例 7-2 registration.py 模块 (原封不动地返回原函数)
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
running register(<function f1 at 0x0000017C0D691268>)
print('registry.len, ', len(registry))
registry
registry.len, 1
[<function __main__.f1()>]
以上证明,装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。
f1()
running f1()
print('再次 registry.len, ', len(registry))
registry
再次 registry.len, 1
[<function __main__.f1()>]
示例 7-2 主要想强调,函数装饰器在导入模块时立即执行,而被装饰的 函数只在明确调用时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。
考虑到装饰器在真实代码中的常用方式,示例 7-2 有两个不寻常的地 方。
- 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装 饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
- register 装饰器返回的函数与通过参数传入的相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。
- 示例 7-2 中的 register 装饰器原封不动地返回被装饰的函数,但 是这种技术并非没有用处。很多 Python Web 框架使用这样的装饰器把函 数添加到某种中央注册处,例如把 URL模式映射到生成 HTTP 响应的函 数上的注册处。这种注册装饰器可能会也可能不会修改被装饰的函数。
7.3 使用装饰器改进“策略”模式
示例 7-3,简化后如下展示:
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
return order
@promotion
def bulk_item(order):
return order if order<20 else order*0.8
order=80
max(promo(order) for promo in promos)
80
print('promos: ', promos)
promos: [<function fidelity at 0x0000017C0D691D90>, <function bulk_item at 0x0000017C0D691B70>]
与 6.1 节给出的方案相比,这个方案有几个优点。
- 促销策略函数无需使用特殊的名称(即不用以 _promo 结尾)。
- @promotion 装饰器突出了被装饰的函数的作用,还便于临时禁用 某个促销策略:只需把装饰器注释掉。
- 促销折扣策略可以在其他模块中定义,在系统中的任何地方都行, 只要使用 @promotion 装饰即可。
不过,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。
为了理解闭包,我们要退后一步,先了解 Python 中的变量作用域。
7.4 变量作用域规则
Python 编译函数的定义体时,它判断 b 是局部变量,因为在 函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本 地环境获取 b。后面调用 f2(3) 时, f2 的定义体会获取并打印局部变 量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。
这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数 定义体中赋值的变量是局部变量。
这比 JavaScript 的行为好多了, JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量 (使用 var),可能会在不知情的情况下获取全局变量。
如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声 明:
b = 6
def f3(a):
global b
print(a)
print(b)
b=9
f3(1)
1
6
b
9
7.5 闭包
示例 7-8 average_oo.py:计算移动平均值
使用类实现,思路: init中初始化一个存储值的数组,call中接受new_value进行滚动计算。
也可以使用函数式实现,使用高阶/嵌套函数实现:
import numpy as np
def make_avg():
values = []
def avg_called(new_value):
values.append(new_value)
return np.mean(values)
return avg_called
avg = make_avg()
avg(233)
233.0
avg(234)
233.5
avg(235)
234.0
在上述的make_avg函数中,values是一个自由变量(free variable)。这是一个 技术术语,指未在本地作用域中绑定的变量,参见图 7-1。
?。png
averager 的闭包延伸到那个函数的作用域之外,包含自由 变量 series 的绑定
审查返回的 averager 对象,我们发现 Python 在 code 属性(表示 编译后的函数定义体)中保存局部变量和自由变量的名称,如示例 7-11
所示。
示例 7-11 审查 make_averager(见示例 7-9)创建的函数
avg.__code__.co_varnames
('new_value',)
avg.__code__.co_freevars
('values',)
print('avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。'
'
这些元素是 cell 对象, 有个 cell_contents 属性,保存着真正的值: ')
avg.__closure__
(<cell at 0x0000017C0E9B90D8: list object at 0x0000017C0D04A588>,)
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定, 这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
7.6 nonlocal声明
Python 3 引入了 nonlocal 声明。
它的作用是把变 量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。
如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更 新。最新版 make_averager 的实现如下:
def make_average():
count = 0
total = 0
def avgager(new_value):
nonlocal count, total
count+=1
total+=new_value
return total/count
return avgager
my_avg = make_average()
my_avg(1)
1.0
my_avg(3)
2.0
示例 7-15 中实现的 clock 装饰器有几个缺点:不支持关键字参数,而 且遮盖了被装饰函数的 name 和 doc 属性。
示例 7-17 使用
functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。
此外,这个新版还能正确处理关键字参数。
示例 7-17 改进后的 clock 装饰器
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*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) -> %r ' % (elapsed, name, arg_str, result))
return result
return clocked
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
ck = clock(factorial)
ck(12)
[0.00000000s] factorial(12) -> 479001600
479001600
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial(12)
[0.00000000s] factorial(1) -> 1
[0.00000000s] factorial(2) -> 2
[0.00000000s] factorial(3) -> 6
[0.00099993s] factorial(4) -> 24
[0.00099993s] factorial(5) -> 120
[0.00099993s] factorial(6) -> 720
[0.00099993s] factorial(7) -> 5040
[0.00099993s] factorial(8) -> 40320
[0.00099993s] factorial(9) -> 362880
[0.00099993s] factorial(10) -> 3628800
[0.00099993s] factorial(11) -> 39916800
[0.00099993s] factorial(12) -> 479001600
479001600
7.8 标准库中的装饰器
Python 内置了三个用于装饰方法的函数:property、classmethod 和 staticmethod。property 在 19.2 节讨论,另外两个在 9.4 节讨论。
另一个常见的装饰器是 functools.wraps,它的作用是协助构建行为 良好的装饰器。
标准库中最值得关注的两个 装饰器是 lru_cache 和全新的 singledispatch(Python 3.4 新增)。 这两个装饰器都在 functools 模块中定义。
7.8.1 使用functools.lru_cache做备忘(cache)
functools.lru_cache 是非常实用的装饰器,它实现了备忘 (memoization)功能。这是一项优化技术,它把耗时的函数的结果保存 起来,避免传入相同的参数时重复计算。
LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。
注:
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache,如示例 7-18 所示。
示例 7-18 生成第 n 个斐波纳契数,递归方式非常耗时:
@clock
def _fibonacci(n):
if n < 2: return n
return _fibonacci(n-2) + _fibonacci(n-1)
_fibonacci(6)
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00100064s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(3) -> 2
[0.00100064s] _fibonacci(4) -> 3
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(3) -> 2
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(3) -> 2
[0.00000000s] _fibonacci(4) -> 3
[0.00000000s] _fibonacci(5) -> 5
[0.00100064s] _fibonacci(6) -> 8
8
import functools
@functools.lru_cache()
@clock
def _fibonacci(n):
if n < 2: return n
return _fibonacci(n-2) + _fibonacci(n-1)
_fibonacci(6)
[0.00000000s] _fibonacci(0) -> 0
[0.00000000s] _fibonacci(1) -> 1
[0.00000000s] _fibonacci(2) -> 1
[0.00000000s] _fibonacci(3) -> 2
[0.00000000s] _fibonacci(4) -> 3
[0.00000000s] _fibonacci(5) -> 5
[0.00000000s] _fibonacci(6) -> 8
8
_fibonacci(6)
8
lru_cache可选参数(maxsize=128, typed=False)
特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名 是
functools.lru_cache(maxsize=128, typed=False)
maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会 被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的 幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保
存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。
顺 便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传 入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它 的所有参数都必须是可散列的。
7.8.2 单分派泛函数(functools.singledispatch装饰器)
假设我们在开发一个调试 Web 应用的工具,我们想生成 HTML,显示不 同类型的 Python 对象。
简单示例如下:
import html
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
这个函数适用于任何 Python 类型,但是现在我们想做个扩展,让它使用 特别的方式显示某些类型。
str:把内部的换行符替换为'<br>
'
;不使用 <pre>
,而是使用 <p>
。
int:以十进制和十六进制显示数字。
list:输出一个 HTML列表,根据各个元素的类型进行格式化。
调用老方法输出为:
print(htmlize(['alpha', 66, {3, 2, 1}]))
<pre>['alpha', 66, {1, 2, 3}]</pre>
singledispatch的设计说明: 间接实现了重载入参
@singledispatch 不是为了把 Java 的那种方法重载带入 Python。在一个类中为同一个方法定义多个重载变体,比在一个函 数中使用一长串 if/elif/elif/elif 块要更好。但是这两种方案 都有缺陷,因为它们让代码单元(类或函数)承担的职责太 多。@singledispath 的优点是支持模块化扩展:各个模块可以为 它支持的各个类型注册一个专门函数。
Python 3.4 新增的 functools.singledispatch 装饰器可以把整体方案 拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。
使用 @singledispatch 装饰的普通函数会变成泛函数(generic function): 根据第一个参数的类型,以不同方式执行相同操作的一组函数。
示例 7-21 singledispatch 创建一个自定义的 htmlize.register 装饰器
把多个函数绑在一起组成一个泛函数的示例:
- @singledispatch 标记处理 object 类型的基函数。
- 各个专门函数使用 @«base_function».register(«type») 装饰。
- 专门函数的名称无关紧要;_ 是个不错的选择,简单明了。
- 为每个需要特殊处理的类型注册一个函数。numbers.Integral 是 int 的虚拟超类。(使用抽象基类检查类型,可以让代码支持这些抽象基类现有 和未来的具体子类或虚拟子类。)
- 可以叠放多个 register 装饰器,让同一个函数支持不同类型。
- 只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral 和 abc.MutableSequence),不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛。例如,Python 扩展可以子类化 numbers.Integral,使用固定的位数实现 int 类型。
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch
def htmlize(obj): # @singledispatch 标记处理 object 类型的基函数。
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str) # 各个专门函数使用 @«base_function».register(«type») 装饰。
def _(text):
content = html.escape(text).replace('
', '<br>
')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>
<li>'.join(htmlize(item)
for item in seq)
return '<ul>
<li>' + inner + '</li>
</ul>'
print(htmlize(['alpha', 66, {3, 2, 1}]))
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>
7.9 叠放装饰器
多个叠放, 从上到下依次进行包含,代码示例如下:
把 @d1 和 @d2 两个装饰器按顺序应用到 f 函数上,作用相当于 f = d1(d2(f))。
@d1
@d2
def f(): print('f')
f()
等同于:
def f(): print('f')
f = d1(d2(f))
7.10 参数化装饰器
Python 把被装饰的函数作为第一个参数传给装 饰器函数。那怎么让装饰器接受其他参数呢?
答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。
示例 7-22 示例 7-2 中 registration.py 模块的删减版,这里再次给 出是为了便于讲解:
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
print('running main()')
print('registry ->', registry)
f1()
running register(<function f1 at 0x0000017C0EBFB598>)
running f1()
running main()
registry -> [<function f1 at 0x0000017C0EBFB598>]
7.10.1 一个参数化的注册装饰器
为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一 个可选的 active 参数,设为 False 时,不注册被装饰的函数。
从概念上看,这个新的 register 函数不是装饰器, 而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用到目标 函数上的装饰器.
示例 7-23 为了接受参数,新的 register 装饰器必须作为函数调用
registry = set()
def register(active=True):
def decorate(func):
print('running register(active=%s)->decorate(%s)' % (active, func))
if active:
registry.add(func)
else:
registry.discard(func) # 删除一个元素,不存在不处理
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
f1()
running register(active=False)->decorate(<function f1 at 0x0000017C0EBFBD08>)
running f1()
@register(active=True)
def f1():
print('running f1()')
f1()
running register(active=True)->decorate(<function f1 at 0x0000017C0EBFB7B8>)
running f1()
register(active=False)(f1)
running register(active=False)->decorate(<function f1 at 0x0000017C0EBFB7B8>)
<function __main__.f1()>
register(active=True)(f1)
running register(active=True)->decorate(<function f1 at 0x0000017C0EBFB7B8>)
<function __main__.f1()>