本文分为如下几个部分什么是闭包
闭包与装饰器
闭包等价——偏函数
闭包等价——类
闭包等价——其他
闭包用于捕获状态值
闭包等价——协程
三种方法实现动态均值
什么是闭包
闭包是携带着一些自由变量的函数。
我们直接来看一个例子
def fun_out(a):
def fun_in(b):
return a + b
return 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 + b
return fun_in
>>> fun1 = fun_out(1)
>>> fun1.__closure__
(,)
另外,有两点需要注意。第一,下面这个fun_in不是闭包,因为其中没有调用外面的a值,调用__closure__会为None
def fun_out(a):
def fun_in(b):
return 1 + b
return fun_in
第二,下面这种情况a和c都会内置进fun_in中,其中c内置的值是3
def fun_out(a):
c = 1
def fun_in(b):
return a + b + c
c = 3
return fun_in
第三,注意区分下面这种情况
def fun_out(a):
return fun_in()
def fun_in():
return a
fun_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
print2 = print1(print2),所以装饰器是闭包的一种应用,自由变量是一个函数,传入和输出使用了相同的变量名。使用print2传入print1是如何调用的、返回的是什么。
闭包等价——偏函数
还是以这个闭包为例
def fun_out(a):
def fun_in(b):
return a + b
return 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 ** 2
def fun_in(b):
return c + b
return fun_in
fun2 = fun_out(2)
之后我们要反复使用fun2这个函数, c只有在定义fun2时计算过,不会在之后的步骤中重复计算,而使用偏函数就无法达到这种效果。这种效果下面的类也是可以达到的。
闭包等价——类
还是上面的例子,这里定义一个类来实现
class Add:
def __init__(self, b):
self.b = b
def __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 = 0
def fun_in(something):
nonlocal count
count += 1
print(count, something)
return fun_in
>>> fun1 = fun_out()
>>> fun1("first")
1 first
>>> fun1("second")
2 second
这种应用可以改写成类的形式但是不能用partial等来改写。不过还有另一种改写方式——协程。
闭包等价——协程
def fun():
count = 0
while True:
something = yield
count += 1
print(count, something)
>>> fun1 = fun()
>>> next(fun1)
>>> fun1.send("first")
1 first
>>> fun1.send("second")
2 second
对yield不了解的读者可以参考这篇文章
三种方法实现动态均值
我们要实现一个函数达到下面的效果
def running_avg(number):
pass
running_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 = 0
self.count = 0
def __call__(self, number):
self.total += number
self.count += 1
return 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 = 0
count = 0
def func(number):
nonlocal total
nonlocal count
total += number
count += 1
return total / count
return func
>>> running_avg = initial()
>>> running_avg(10)
10.0
>>> running_avg(20)
15.0
>>> running_avg(30)
20.0
3.协程实现
def Running_avg():
total = 0
count = 0
average = None
while True:
number = yield average
count += 1
total += number
average = 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章
专栏信息
专栏目录:目录