Python 入门(七)- 闭包专题

在大多数其他语言中,函数只是一段可执行的代码,并不是对象。一旦被执行,就被固化了,常驻于内存。因此,函数不能被实例化的。

但是在python中,一切皆对象(类)。

函数可以赋值给一个变量,可以作为参数传递给另外一个函数,也可以把一个函数当作另外一个函数的返回结果。

1
2
3
4
def a():
pass

print(type(a)) # <class 'function'>

我们注意到,在上例中,函数a()是一个类(class)。

什么是闭包

闭包的概念和变量作用域相关,定义为:由函数和定义时的外部环境变量叫做闭包,调用时不再受其他的变量影响。

闭包 = 函数+环境变量

下面的写法称为“闭包”,即

1
2
3
4
5
6
7
8
9
10
def curve_pre():
a = 25

def curve(x):
return a*x*x

return curve

f = curve_pre() # 返回的是个公式,并非结果
print(f(2)) # 100

看起来上边的例子顺理成章。如果我们改成下边的写法,将a的赋值a=10写在函数外边,看似f=curve_pre()返回的是f就是curve()函数,那么此时我们得到的结果应该是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def curve_pre():
a = 25

def curve(x):
return a*x*x

return curve

a = 10
f = curve_pre()
print(f(2)) # 结果是多少呢???

### 看似等价于下面的写法
a = 10
def f1(x):
return a*x*x

print(f1(2)) # 40

在第一种写法中,看似也应该得到结果40。但在第一个结果中我们得到了100!

这就是闭包的现象。

尽管curve()没有执行,只是作为返回结果。但当在模块中执行时,参数不会取模块中的变量,而是依然取定义时环境变量a=25。因此,函数curve(x)与定义函数时的环境变量a=25形成了一个闭包。

由此,定义一下闭包:

由函数和定义时外部环境变量共同构成的叫做闭包。一旦闭包形成好,这个函数在任何时候被调用都不受重新赋值影响。

特别需要注意的是,环境变量一定在函数定义的外部,且不能为全局变量。举个反例,在下面的例子中,a=25为全局变量,所以 curve(x)a=25并没有形成一个闭包,最终结果为40。

1
2
3
4
5
6
7
8
9
10
a = 25
def curve_pre():
def curve(x):
return a*x*x

return curve

a = 10
f = curve_pre()
print(f(2)) # 40

那么,闭包的环境变量存在哪里了呢?如何获取呢?

答:内置变量 f.__closure__,定义时的环境变量在f.__closure__[0].cell_contents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def curve_pre():
a = 25

def curve(x):
return a*x*x

return curve

a = 10
f = curve_pre()

print(f(2)) # 100

print(f.__closure__) # (<cell at 0x105a3f0a8: int object at 0x105913f80>,)
print(f.__closure__[0].cell_contents) # 25

闭包的意义

闭包的意义在于保存了函数定义时的环境(现场)。若没有闭包的存在,则很容易受外部环境的影响,难以保证函数运行的正确进行。

闭包的经典误区

在不同的环境下将是不同的闭包。使用闭包时需要注意的几点:

1
2
3
4
5
6
7
8
9
10
11
12
def f1():
a = 10

def f2():
a = 20 # 函数内部重新赋值
print(a) # 20 —— ②

print(a) # 10 —— ①
f2()
print(a) # 10 —— ③

f1()

上述程序将会按照①②③的顺序进行输出,

① 环境变量为10
② 执行f2(),相当于执行闭包,a=20
③ 重点。执行f2()是不会影响a=20被python认为是局部变量,局部变量不会影响外部变量的

上边是不是一个闭包呢?答案是:不是。我们可以通过输出 f.__closure__ 验证一下。

1
2
f = f1() # f变量用于接收结果
print(f.__closure__) # AttributeError: 'NoneType' object has no attribute '__closure__'

特别注意,以下几种写法均不是闭包,需要与闭包严格区分。

误区一

返回变量。如果我们返回a会报错,不是闭包。

1
2
3
4
5
6
7
8
def f1():
a = 10
def f2():
a = 20
return a # 返回a

f = f1()
print(f.__closure__) # 'int' object has no attribute '__closure__'

误区二

函数内部a被赋值,则a会被认为是局部变量,不再会引用外部的环境变量。

1
2
3
4
5
6
7
8
def f1():
a = 10
def f2():
a = 20 # a被认为是局部变量
return f2

f = f1()
print(f.__closure__) # None

正确的写法

与是否返回无关,但必须要引用a。如下例中c=20*areturn a的写法均可。

1
2
3
4
5
6
7
8
9
10
def f1():
a = 10
def f2():
c = 20*a # 正确写法
return a # 也是正确写法

return f2

f = f1()
print(f.__closure__) # (<cell at 0x1010d20a8: int object at 0x100fddda0>,)

闭包的用处

需要注意的是,闭包并不是必不可少的,它只是函数式编程的一种写法。在实际应用过程中,是否使用闭包是根据需求而定的。

下面我们通过一个问题来了解闭包在实际问题中的应用。

假设有个旅行者,x=0代表地标中的起点,通过调用函数计算旅行者当前的位置。例如:旅行者走了3步,结果为3;旅行者继续走了5步,则结果为8。

我们不难发现,这一问题的关键在于:每次调用函数时,需要保存上一次结果状态。

非闭包的写法

先看一下常规思路的写法:

1
2
3
4
5
6
7
8
origin = 0
def go(step):
new_pos = origin + step
origin = new_pos

print(go(2)) # 2
print(go(3)) # 5
print(go(6)) # 11

代码会报错:local variable 'origin' referenced before assignment。按理解,当我们执行:

new_pos = origin + step

origin内部没有变量,则会沿着作用域链向上寻找。但由于我们写了 origin = new_pos,在定义时,等号左边的变量会被认为是局部变量,默认此时的变量在函数内部存在的,就不会在外部寻找了。

所以当我们注释掉origin = new_pos,代码将不再报错,但origin在外部寻找,输出结果为:

1
2
3
print(go(2)) # 0
print(go(3)) # 0
print(go(6)) # 0

我们期望的是:函数中的origin作为全局变量来使用。所以加上global关键字即可。

1
2
3
4
5
6
7
8
9
10
origin = 0
def go(step):
global origin
new_pos = origin + step
origin = new_pos
return new_pos

print(go(2)) # 2
print(go(3)) # 5
print(go(6)) # 11

闭包的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
origin = 0
def factory(pos):
def go(step):
# nonlocal pos # 强制声明不是局部变量
new_pos = pos + step
pos = new_pos
return new_pos
return go

tourist = factory(origin)
print(tourist(2))
print(tourist(3))
print(tourist(6))

代码报错:local variable 'pos' referenced before assignment,是因为 pos = new_pospos被认为是局部变量,在使用前new_pos = pos + step未声明。

在此引入nonlocal,可强制声明某变量不是局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
origin = 0
def factory(pos):
def go(step):
nonlocal pos # 强制声明不是局部变量
new_pos = pos + step
pos = new_pos
return new_pos
return go

tourist = factory(origin)
print(tourist(2)) # 2
print(tourist(3)) # 5
print(tourist(6)) # 11

尽管闭包(函数式编程)的思维方式很烧脑,但我们仍然推荐闭包的写法。因为过多使用全局变量是糟糕的行为,函数无法保证自封闭性,就缺少了它存在的意义。在闭包的写法中,全局变量origin始终为0,我们并没有对全局变量进行更改。

小结

1.闭包是工厂模式
2.环境变量常驻内存,容易造成内存泄露。
3.闭包python和javascript,python强调环境变量,javascript的切入点则是可实现函数外部调用函数内部的变量。

我们仍使用之前的例子:

1
2
3
4
5
6
7
8
def curve_pre():
a = 25
def curve(x):
return a*x*x

return curve

f = curve_pre()

我们调用f的时候,实质是调用curve()函数,但curve()内部引入了环境变量a,对于外部模块,a是个局部变量,则实现了模块级别(函数外部)调用函数内部的变量的机制。