Python函数拾遗 函数装饰器与闭包 --流畅的python
本章的notebook文件在这里
函数装饰器与闭包
装饰器基础知识
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数). 装饰器可能会处理被装饰的函数并返回, 或者将其替换为另一个函数或者可调用对象.
例如,下面两段代码是等价的:
1 |
|
你是猪
可以发现, 调用target
函数后, 打印出来的并不是’哈哈哈’, 而是’你是猪’, 这就是因为deco
装饰器在中途掉包了这个函数.
当然装饰器是一种语法糖,但是它确实很好用, 其特性有二:
- 能把被装饰的函数替换为其他函数
- 装饰器在加载模块时立即执行
所谓装饰器在加载时立即执行是指, 装饰器本身会被执行, 你用该装饰器装饰了几个函数就会被执行几次, 但是需要注意的是,被装饰的函数本身并不会被执行.
利用装饰器改进策略模式
下面我们装饰器改进一下之前提到的电商促销折扣的代码, 这里的一个问题是,我们每次新增折扣策略的时候,可能会忘记将它加入策略列表中而造成错误, 利用装饰器可以这么写:
1 | # BEGIN STRATEGY_BEST4 |
我们定义一个会将传入的函数加入策略列表的装饰器, 并用该装饰器去装饰每个定义的策略.
当然, 通常我们使用装饰器时会改变传入的函数, 而不是仅仅将它原样返回, 为此我们需要先了解闭包和python的变量作用域.
变量作用域规则
python并不要求声明变量, 但是他会假定在函数定义体中的复制变量为局部变量. 即时之前定义了一个同名的全局变量, python解释器也会认为其为局部变量, 例如:
1 | b = 6 |
3
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-3-7bc160830b6f> in <module>()
6 b = 9
7
----> 8 f(3)
<ipython-input-3-7bc160830b6f> in f(a)
3 def f(a):
4 print(a)
----> 5 print(b)
6 b = 9
7
UnboundLocalError: local variable 'b' referenced before assignment
上述代码中在print(b)
的时候就会出错, 虽然之前已经给b
赋过值, 但是由于b
在f
函数内出现过, 因此会被认为是局部变量, 要解决这个问题, 需要在函数f
内部将b声明为global.
1 | b = 6 |
3
6
闭包
闭包在python中指的是延伸了作用域的函数, 其中包含函数定义体中引用,但是不在定义体中定义的非全局变量.
听上去很拗口, 具体举个例子就容易明白了.下面定义一个高阶函数来计算序列的平均值.
1 | def make_average(): |
10.0
1 | avg(11) |
10.5
1 | avg(12) |
11.0
那么很好奇一点,当make_average
返回averager
函数后, 我们序列中的历史值是存在哪儿的呢?
观察averager
函数中, series
于它是一个自有变量, 即没有绑定在本地作用域的变量, 但是我们发现每次avg
的时候它都能访问到该变量,我们审查一下该变量:
1 | avg.__code__.co_varnames |
('new_value', 'total')
1 | avg.__code__.co_freevars |
('series',)
1 | avg.__closure__ |
(<cell at 0x7f803f933dc8: list object at 0x7f803e859f88>,)
1 | avg.__closure__[0].cell_contents |
[10, 11, 12]
可以发现, series
以自由变量的形式保存在__closure__
属性中. avg.__closure__
的各个元素对应于avg.__code__.co_freevars
中的一个名称, 这些元素是cell
对象, 有一个cell_contents
对象其中保存着真实的值.
现在回头看这个averager, 可以发现我们其实只用到了这些数的和和计数, 而不用整个数值列表, 很自然想到做以下修改:
1 | def make_average(): |
但是这么写会有有问题, 原因在于, averager
里对count
赋值了,这会将其变成一个局部变量, 而不再是自由变量, 此时我们需要将其加上一个nonlocal
声明.
1 | def make_average(): |
10.8
标准库中的装饰器
下面介绍一下两个标准库中的装饰器, functools.lru_cache
和singledispatch
使用functools.lru_cache 做备忘
functools.lru_cache
可以用来实现备忘功能,它将耗时的函数的结果保存起来,避免传入相同的参数时重复计算.
LRU即为”Least Recently Used”, 表示会保存最近的缓存, 一段时间不用后的缓存则会被扔掉.
一个适用的场景是递归函数, 例如斐波那契数列的计算. 计算斐波那契数列时, 假设计算f(n), 我们需要先计算f(n-1)和f(n-2), 递推下去其实有很多项是重复计算的, 使用lru_cache
就会将中间函数的结算结果缓存, 这样每一项都会只计算一次.
具体代码如下:
1 | import functools |
单分派泛函数 singledispatch
在写代码时,我们常常会遇到这样的需求,即函数处理的方法根据传入的参数类型不同而有所不同,例如构想一个打印函数传入的如果是:
- 字符串 str 直接打印
- 整数 int 以十六进制打印
- 日期对象 Date 以’MM-DD in YYYY’的格式打印
python不支持重载函数,而使用一连串的if/else来判断再调用相应的函数又显得太过笨拙了, 此时我们可用singledispatch
来处理该问题.我们用该装饰器将整体方案拆分成多个模块,并根据不同的参数类型来执行同一组函数. 被装饰的函数叫做泛函数.
1 | from functools import singledispatch |
1 | # 字符串 按照默认的分派 |
'打印出来是: 哈哈哈啊'
1 | # 整数 |
'1024 (0x400)'
1 | # date对象 |
'11-11 in 2018'
1 | # list 以及其他未被注册分派的参数类型都会按照泛函数默认方式执行 |
"打印出来是: ['1']"
值得提一句的是, 这里的装饰器是可以叠放的, 如上例中, 我们要以处理整数的方式同样处理浮点数, 则可以在函数上叠放两个装饰器.
将@d1
, @d2
两个装饰器按顺序应用到f
上,作用相当于 f=d1(d2(f))
参数化装饰器
有时我们需要给装饰器传入某个参数, 此时则先创建一个装饰器工厂函数, 将参数传给它再返回一个装饰器.
举例说明:
1 | registry = set() |
running register(active=False)->decorate(<function f1 at 0x7f803e7dd950>)
running register(active=True)->decorate(<function f2 at 0x7f803e7ddbf8>)
1 | # 运行f1 程序不会将f1加入registry,因为active为False |
running f1()
{<function __main__.f2>}
1 | f2() |
running f2()
<function __main__.register>