首页 > 代码库 > Python 3 函数自由变量的大坑

Python 3 函数自由变量的大坑

Python中函数是一个对象, 和整数,字符串等对象有很多相似之处,例如可以作为其他函数的参数或返回对象, Python中的函数还可以携带自由变量, 两者无疑极大增进了Python的表达力.

但是Python函数自由变量的内部机制和列表解析或for循环结合使用时却暗藏杀机:

#---CASE 1fs = map(lambda i:(lambda j: i*j),range(6))print([f(2) for f in fs])#---CASE 2fs = [lambda j:i*j for i in range(6)]print([f(2) for f in fs])#---CASE 3fs = []for i in range(6):    fs.append(lambda j:i*j)    if i==3:        breakprint([f(2) for f in fs])#---CASE 4fs = [(lambda i:lambda j:i*j)(i) for i in range(6)]print([f(2) for f in fs])

 

结果:

[0, 2, 4, 6, 8, 10][10, 10, 10, 10, 10, 10][6, 6, 6, 6][0, 2, 4, 6, 8, 10]

 

可以通过下面这个简单的测试来分析Python函数在执行时是如何确定自由变量的值的:

i = 1def f(j):    return i*jprint(f(2)) # ---> 2i = 2print(f(2)) # ---> 4def g():    i = 3    def f(j):        return i*j    return ff = g()print(f(2)) # ---> 6i = 100print(f(2)) # ---> 6

 

可见,当 函数f在*定义时*, Python不会记录自由变量‘i‘对应什么对象, 只会告诉f, 你有一个自由变量, 它的名字叫 ‘i‘.

接着, 当函数f在*执行时*, Python告诉f:
(1) 空间上: 你需要在你被*定义时*的外层namespace里面去查找i对应的对象, 假设这个namespace为X.

(2) 时间上: 是在你*当前运行时*, X 里面的 i 对应的对象. 

上面那个简单测试中的 i = 2 之后, f(2)随之也返回4也能反映了这一点.

CASE 2和3 也是如此, fs里面每个函数对应的自由变量i在*定义时*都是循环变量i, 因此*执行时*都是对应循环结束或跳出时i所指对象.

而 CASE 1和4为什么能如愿发生变化呢?  这是因为函数对应的自由变量i不再是循环变量i, 而是外层lambda函数*执行时*,循环变量i所指对象在其栈上的拷贝,  由于每次调用外层lambda时i所指对象都不相同, 因此每个函数的自由变量也会指向不同的对象.

 

最后, 列表解析里面的作用域是一个全新的作用域,  而普通的for循环则有所不同. 例如:

#---CASE 2fs = [lambda j:i*j for i in range(6)]print([f(2) for f in fs])i = 4print([f(2) for f in fs])#---CASE 3fs = []for i in range(6):    fs.append(lambda j:i*j)print([f(2) for f in fs])i = 4print([f(2) for f in fs])

结果是:

[10, 10, 10, 10, 10, 10][10, 10, 10, 10, 10, 10][10, 10, 10, 10, 10, 10][8, 8, 8, 8, 8, 8]