浅谈Python闭包和late binding机制

从一个小问题开始

前几天在写代码的时候需要在循环中定义函数,却出现了预料之外的现象。先来看看下面这段简单的代码:

def generate_funcs():
	funcs = []
	for i in range(5):
		funcs.append(lambda: print(i))
	return funcs
	
for f in generate_funcs():
	f()

在循环过程中定义函数,我们希望5个函数分别输出0、1、2、3、4,即期望的输出是这样的:

0
1
2
3
4

然而运行结果是下面这样的:

4
4
4
4
4

大家不禁会纳闷儿了,我定义函数的时候明明白白写着 print(i)i 的值从 0 循环到 4,怎么执行出来就打印的都是 4 了呢?没错,这就是 Python 闭包中的 late binding 机制在“作祟"。这篇文章我们就好好讲讲 Python 闭包和 late binding 机制,避免大家再次踩坑。

从闭包 (closure) 谈起

我们要先介绍嵌套函数 (nested function) 和非局部变量 (non-local variable) 的概念。

定义在函数内部的函数叫做嵌套函数。我们都知道,函数有作用域,嵌套函数既可以访问自身作用域的变量,也可以访问外层函数作用域中的变量。看一个例子:

def print_function(msg):
	# printer is a nested function
    def printer():
        print(msg)
    printer()
    
print_function("Hello world!")

输出为

Hello world!

这里 printer 是一个嵌套函数,通过程序输出我们可以发现 printer 可以访问到外层函数 print_function 作用域中的 msg 变量。我们通常把函数参数或者定义在函数内部的变量叫做函数的局部变量 (local variable),而对于 printer 这个嵌套函数,msg 可以被访问到,但并不是 printer 的局部变量(而是外层函数的局部变量),我们把 msg 这样被嵌套函数访问的外层函数的局部变量称为嵌套函数的非局部变量 (non-local variable)。

我们把上面的程序稍作修改,得到下面的程序:

def print_function(msg):
	# printer is a nested function
    def printer():
        print(msg)
    return printer
    
p = print_function("Hello world!")
p()

输出依然是

Hello world!

Amazing!纵使我们在得到 p 的时候 print_function 已经执行结束了,msg 看似已经从内存中消失了,但 p 依然能够访问到 msg 这个非局部变量!

这,就是闭包的力量。

return printer 语句返回的不仅仅是一个函数,还有一个闭包,这个闭包里保存了函数要用到的所有非局部变量。事实上,我们可以通过函数对象的 __closure__ 方法查看它的闭包:

>>> p.__closure__
(<cell at 0x7eff9df1ef18: str object at 0x7eff9c5b1630>,)

正像我们之前所说,p 的闭包中存放了它需要用到的非局部变量 msg

闭包有很广泛的应用,我们可以用闭包来减少全局变量的定义进而提供数据隐藏,还可以提供面向对象的编程风格,使代码实现更优美。但这并不是本文的重点,在此不再详述。

late binding 机制

终于进入正题了,late binding 机制是怎么回事?为什么函数的运行结果不符合我们的期望?我们先来看一下 Python 手册中对于 late binding 机制的解释:

Python’s closures are late binding. This means that the values of variables used in closures are looked up at the time the inner function is called.

译:Python 闭包是 late binding 的,即,闭包中用到的变量的值是在内层函数执行的时候才去查询的。

原来如此!这时候我们就有一种直觉,问什么第一个例子中输出的全都是 4?因为在每一个 f 函数执行的时候,for i in range(5) 已经执行完毕,i 的值被固定在了 4。我们通过查看闭包内容进一步证明我们的猜想:

def generate_funcs():
	funcs = []
	for i in range(5):
		funcs.append(lambda: print(i))
	return funcs
	
for f in generate_funcs():
	f()
	print(f.__closure__)
	print('cell value {}'.format(f.__closure__[0].cell_contents))

输出如下

4
(<cell at 0x7f0146578f18: int object at 0x8d5020>,)
cell value 4
4
(<cell at 0x7f0146578f18: int object at 0x8d5020>,)
cell value 4
4
(<cell at 0x7f0146578f18: int object at 0x8d5020>,)
cell value 4
4
(<cell at 0x7f0146578f18: int object at 0x8d5020>,)
cell value 4
4
(<cell at 0x7f0146578f18: int object at 0x8d5020>,)
cell value 4

果不其然!这些函数的闭包里都是同一个变量,也就是值已经固定在 4 的那个循环变量 i程序并没有在编译期计算出 i 的值,而只是保存了 i 的引用,在运行期获取 i 的值,这导致了上面的结果。

解决方法

我们已经清楚了 late binding 机制导致上面输出结果的原理,那么有没有办法让程序输出期望的结果呢?答案是肯定的。下面几种方法都是可以的:

使用函数工厂模式(推荐)

def make_printer(i):
	def printer():
		print(i)
	return printer
	
def generate_funcs():
	funcs = []
	for i in range(5):
		funcs.append(make_printer(i))
	return funcs
	
for f in generate_funcs():
	f()

使用函数默认参数

def generate_funcs():
	funcs = []
	for i in range(5):
		funcs.append(lambda i=i: print(i))
	return funcs
	
for f in generate_funcs():
	f()

使用 yield 方法

def generate_funcs():
	for i in range(5):
		yield lambda i=i: print(i)
		
for f in generate_funcs():
	f()

使用 functools 模块中的 partial 方法

from functools import partial

def generate_funcs():
	funcs = []
	for i in range(5):
		funcs.append(partial(print, i))
	return funcs
	
for f in generate_funcs():
	f()

使用元组

def generate_funcs():
	funcs = (lambda: print(i) for i in range(5))
	return funcs
	
for f in generate_funcs():
	f() 

上面方法的原理都是让程序在编译期获取 i 的值,从而得到我们期望的输出:

0
1
2
3
4
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章