一.起因
前几天给自己写了一个应用,界面的展示用到了python的tkinter,过程中遇到一个问题,我要给界面中的按钮绑定函数,由于按钮较多而功能类似,而且只能绑定没有参数的函数名,可我需要参数。所以我的策略是for循环加lambda函数,lambda函数内调用带参数的长函数,靠for循环的顺序作为参数传入,函数内部映射以实现响应功能。
可惜出现了个bug,那就是添加到列表里的函数,分别调用后都是相同的结果。
将问题聚焦,简化得到如下模型。
二.BUG出现
lst = []
def go(item):
print(item)
for i in range(3):
lst.append(lambda:go(i))
for fn in lst:
fn()
2
2
2
我希望他出现的是0,1,2
如上图,只需要让添加到列表里的每一个匿名函数在调用时,产生的结果都不同就基本成功了。
接下来就是怎么解决了。
三.浅拷贝试试。
import copy
lst = []
def go(item):
print(item)
for i in range(3):
buf = copy.copy(i)
lst.append(lambda :go(buf))
for fn in lst:
fn()
2
2
2
不得行。(这时lst里每一个函数的内存地址都是不同的,肯定是参数的问题)
随后我反应过来,只要lambda函数里带有变量名i,那么这个函数调用时一定会找到i这个全局变量,而容器里的每一个函数调用结果,势必都会相同。
也就是说只要参数和i有关,就极大可能出错,而如果和i完全无关,那就上游数据都没了,不可能实现功能。
所以想到了局部变量来解决,代码如下。
四.解决方法
lst = []
def go(item):
print(item)
for i in range(3):
def go2(i): # 函数摆在这里理解快,扔上面更好
lst.append(lambda: go(i))
go2(i)
for fn in lst:
fn()
#结果
0
1
2
成功了。
这段看起来是在废话的代码实际上还是挺能办事的,把核心代码函数封装再调用,而且到现在我都没想到第二种方法,事后也在网上找过,没有对这种问题的处理方法。
五.解释
之前的错误是因为每一个添加的函数的实参都指向了同一个全局变量,但我需要的是n个不同的变量,而不是一个。函数内的局部变量可以完美解决这个问题,我们不必关心每次调用go2的局部变量名是什么,尽管看起来都叫i,但在解释器眼里都不同就对了。
在容器中添加带变量名的容器,和直接添加无依赖的变量是不同的。后者很容易在循环里进行添加,而前者如果是在循环中,就必须考虑这个带变量名的容器,其变量指向的内存地址在设计逻辑中是否可变,如果可变,那么不必管。如果不可变,那么最好用局部变量的方式传递。因为解释器已经专为这种只使用一次的变量写好了整个生命周期,一般局部变量会在作用域结束后消亡,但如果你在其消亡前将他给了其他对象作为引用,不怕,解释器有引用计数器去管理内存,只要还有别的对象引用你,这个局部变量的内存地址将会保存给他。
之后问题就解决了,就是这么个小问题扔到项目里就是一百多行的排查,不知道具体症结的时候还以为是类继承修改的时候破坏掉什么东西了,解决完之后真是血脉舒畅。
六.总结
都说函数是为了增加代码复用率,提高扩展性。但其实还有一个重要的作用,就是将全局变量引入到局部变量,解决全局变量双拳难敌四手的问题。