本文分为如下几个部分
- 什么是闭包
- 闭包与装饰器
- 闭包等价——偏函数
- 闭包等价——类
- 闭包等价——其他
- 闭包用于捕获状态值
- 闭包等价——协程
- 三种方法实现动态均值
什么是闭包
闭包是携带着一些自由变量的函数。
我们直接来看一个例子
def fun_out(a):def fun_in(b):return a + breturn fun_in
fun1 = fun_out(1)
其中fun1
函数是一个闭包,它携带着fun_out
中定义的a
变量,值为1。运行程序的效果是这样的
>>> fun1 = fun_out(1)
>>> fun1(3)
4
>>> fun1(6)
7>>> fun5 = fun_out(5)
>>> fun5(3)
8
>>> fun5(6)
11
fun1
和fun5
两个函数的定义相同,只是携带的自由变量不同,便成为了两个函数。由此闭包可以作为函数工厂,生产出功能类似,但是会有细微差别的函数。
闭包用起来非常直观,它反映了函数内调用一个变量的搜索路径,fun_in
中调用某变量会
- 先在
fun_in
函数定义的局部空间中开始搜索,可以找到b
- 如果在局部空间中找不到,则在更上层的局部空间中找,可找到
a
- 之后在全局空间中搜索,即函数外定义的变量
- 如果还是找不到会去找python内置变量
- 如果还是找不到则抛出异常
闭包的特点就是,如果单独看fun_in
函数的定义,不看周围环境,则a
是未被定义的;而用这种方式返回则可以将a
的值内置到函数中,这就是fun_in
与fun1
的区别。
我们可以查看闭包中绑定的自由变量
def fun_out(a):def fun_in(b):return a + breturn fun_in>>> fun1 = fun_out(1)
>>> fun1.__closure__
(<cell at 0x000001BC7FD3D768: int object at 0x0000000072C96C40>,)
另外,有两点需要注意。第一,下面这个fun_in
不是闭包,因为其中没有调用外面的a
值,调用__closure__
会为None
def fun_out(a):def fun_in(b):return 1 + breturn fun_in
第二,下面这种情况a
和c
都会内置进fun_in
中,其中c
内置的值是3
def fun_out(a):c = 1def fun_in(b):return a + b + cc = 3return fun_in
第三,注意区分下面这种情况
def fun_out(a):return fun_in()def fun_in():return afun_out(1)
如果fun_in
不是定义在fun_out
里面,则fun_in
在寻找变量a
时,不会找fun_out
的局部空间,而是直接到全局去找。如果代码只是像上面这样定义,则调用fun_out
会报错,因为fun_in
找不到a
这个变量;除非在全局定义一个a
才能找到。
闭包与装饰器
装饰器是闭包的一个应用,只是携带的自由变量是一个函数
def print1(func):def wrapper(*args, **kw):print(1)return func(*args, **kw)return wrapper@print1
def print2():print(2)>>> print2()
1
2
@print1
命令等价于print2 = print1(print2)
,所以装饰器是闭包的一种应用,自由变量是一个函数,传入和输出使用了相同的变量名。使用@print1
的好处是使编程思路更加直观,定义好装饰器后,如果想对这个函数添加这个功能,就装饰上去,而不用重新思考print2
传入print1
是如何调用的、返回的是什么。
闭包等价——偏函数
还是以这个闭包为例
def fun_out(a):def fun_in(b):return a + breturn fun_in
fun1 = fun_out(1)
下面使用偏函数实现
from functools import partial
def fun(a, b):return a + b>>> fun1 = partial(fun, b=1)
>>> fun1(3)
4
>>> fun5 = partial(fun, b=5)
>>> fun5(3)
8
偏函数即固定某些参数的取值,达到函数工厂的作用。
但是这种方法不够灵活,比如下面一种情况
def fun_out(a):c = a ** 2def fun_in(b):return c + breturn fun_in
fun2 = fun_out(2)
之后我们要反复使用fun2
这个函数, c
只有在定义fun2
时计算过,不会在之后的步骤中重复计算,而使用偏函数就无法达到这种效果。这种效果下面的类也是可以达到的。
闭包等价——类
还是上面的例子,这里定义一个类来实现
class Add:def __init__(self, b):self.b = bdef __call__(self, a):return a + self.b>>> add1 = Add(1)
>>> add1(3)
4
>>> add5 = Add(5)
>>> add5(3)
8
类相比于普通函数的一个好处是,类可以将一些变量内置进去,让类中定义的函数随意调用和修改。类相比于闭包的好处在于,函数运行结束后,可以随意调用修改属性(自由变量);比如一个递归函数,想查看这个函数被调用了几次。
闭包等价——其他
1.如果只是使用普通的函数,传入两个参数也可以达到类似的效果
def fun(a, b):return a + b
fun(3, b=1)
但是调用时太麻烦了,应该不算实现了闭包的功能。
2.使用lambda表达式
def fun(a, b):return a + b>>> fun1 = lambda a: fun(a, 1)
>>> fun1(3)
4
>>> fun5 = lambda a: fun(a, 5)
>>> fun5(3)
8
相比于partial来说,lambda表达式会略显臃肿。
闭包用于捕获状态值
除了函数工厂,闭包还可以用于将函数与它的状态绑定,方便输出函数的状态和函数运行结果,比如输出函数被调用的次数
def fun_out():count = 0def fun_in(something):nonlocal countcount += 1print(count, something)return fun_in>>> fun1 = fun_out()
>>> fun1('first')
1 first
>>> fun1('second')
2 second
这种应用可以改写成类的形式但是不能用partial等来改写。不过还有另一种改写方式——协程。
闭包等价——协程
def fun():count = 0while True:something = yieldcount += 1print(count, something)>>> fun1 = fun()
>>> next(fun1)
>>> fun1.send('first')
1 first
>>> fun1.send('second')
2 second
对yield
不了解的读者可以参考这篇文章
三种方法实现动态均值
我们要实现一个函数达到下面的效果
def running_avg(number):passrunning_avg(10) # 10 --> 10 / 1
running_avg(20) # 15 --> (10 + 20) / 2
running_avg(30) # 20 --> (10 + 20 + 30) / 3
1.类实现
class Running_avg:def __init__(self):self.total = 0self.count = 0def __call__(self, number):self.total += numberself.count += 1return self.total / self.count>>> running_avg = Running_avg()
>>> running_avg(10)
10.0
>>> running_avg(20)
15.0
>>> running_avg(30)
20.0
2.闭包实现
def initial():total = 0count = 0def func(number):nonlocal totalnonlocal counttotal += numbercount += 1return total / countreturn func>>> running_avg = initial()
>>> running_avg(10)
10.0
>>> running_avg(20)
15.0
>>> running_avg(30)
20.0
3.协程实现
def Running_avg():total = 0count = 0average = Nonewhile True:number = yield averagecount += 1total += numberaverage = total / count>>> running_avg = Running_avg()
>>> next(running_avg)
>>> running_avg.send(10)
10.0
>>> running_avg.send(20)
15.0
>>> running_avg.send(30)
20.0
参考资料
下面是本文的参考资料
- 这篇文章对什么是闭包解释的非常清楚
- 闭包的应用和替代方案
- 闭包捕获状态值
- 动态均值参考fluent python一书第16章
专栏信息
专栏主页:python编程
专栏目录:目录
版本说明:软件及包版本说明