问题情景
用pymoo: Multi-objective Optimization in Python这个包在Python中作多目标优化的时候,要用函数列表来定义多目标优化问题的多目标函数(pymoo - Definition),形如:
objs=[
lambda x:np.sum((x-2)**2),
lambda x:np.sum((x+2)**2)
]
上面这是官方文档的示例,实际上每个元素也可以直接是用户自定义的函数对象。而我的应用情景中,则是用lambda表达式创建匿名函数对象,并append()加入objs列表中:
objs = [obj_A]
for idx in range(6):
objs.append(lambda params: obj_B(params, idx))
其中obj_A是已有的一个目标函数,形如func(params)且输出为单个值,那么既然要构建多目标函数列表(每个元素对应一个目标),每个子目标函数的形式自然要统一,而obj_B比较特殊,它其实只要求参数变量params中的一个维度,(params为六维,所以obj_B最后会生成六个子目标函数),所以这里用lambda函数重构出每一个子目标函数。而表达式中idx属于lambda函数外的scope,lambda函数将会访问函数体外部的变量,这就说Python中的“闭包”(closure),这是没有问题的,但是使用时发现下面的问题:
一个问题是,lambda函数对象建立后,在后续的调用时,idx是这个变量的引用,还是定义函数时idx变量的值?很遗憾,和Python中的很多情形一样,这里lambda函数使用的是引用。而for循环体结束后,idx变量依然存在且值为5,所以,这时ojbs[1]到ojbs[6]中的lambda函数对象虽然确实是不同的函数对象,但执行时全都相当于obj_B(params, 5)。
问题分析及解决方法
一开始首先想到的是利用copy.deepcopy(),简单粗暴。不过在Python Cookbook Recipes for Mastering Python 3, 3rd Edition的7.7 Capturing Variables in Anonymous Functions发现了另一种解决方案:
objs = [obj_A]
for idx in range(6):
objs.append(lambda params, idx_p=idx: obj_B(params, idx_p))
通过在lambda函数列表加入一项default argument来传入idx的值,这里把两个idx_p改成idx也是一样的。同时由于缺省参数的性质,这些匿名函数的参数是可以只有一个的。那么用这种方式就可以顺利解决这一问题了。
小结
这个案例可见,在Python函数中使用闭包一定要慎重,lambda函数虽方便,但稍有不慎很容易产生各种问题。