Python和Ruby中each循环引用变量问题(一个隐秘BUG?)
虽然这个问题我是在Python里遇到的,但是用Ruby解释起来比较容易一些。在Ruby里,遍历一个数组可以有很多种方法,最常用的两种无非是for和each:
arr=['a','b','c'] arr.each{|e| putse } foreinarr putse end
通常我比较喜欢后者,似乎因为写起来比较好看,不过从效率上来说前者应该会稍微快一点,因为后者实际上是在遍历的过程中对每个元素都调用一个lambda函数来做的,虽然一般情况下并不明显,不过设置上下文并调用函数确实是有开销的,特别是在动态语言里面(不考虑JIT内联优化的话)。不过这次的问题并不是性能。然而确实跟“each对每个元素都会新建一个scope而for则不是”有关。
看下面一段代码:
arr=['a','b','c'] h1=Hash.new h2=Hash.new arr.each{|e| h1[e]=lambda{e+'!'} } foreinarr h2[e]=lambda{e+'!'} end h1['a'].call#=>? h2['a'].call#=>?
两个call分别会得到什么?应该已经猜到了吧?分别是'a!'和'c!',后者之所以是'c!'是因为for并没有在循环的每一步都重新创建一个scope,因此三个lambda的closure引用到了同一个变量,而这个变量在最后一次被赋值为'c',所以导致了这样的后果。
问题其实出自我在用Python写的一个小程序中的一段,代码类似于这样:
forpropinpublic_props: setattr(proxy,'get_%s'%prop,lambda:self.get_prop(prop))
其中proxy是我提供的一个代理对象,将self的一些公开的属性给暴露出去,因为要限制对非public的属性的访问,我并不想在这个proxy中存放任何到self的引用,否则在没有访问权限限制的Python里通过类似proxy._orig_self.some_private_prop的方式来访问是轻而易举的。所以最后选择了上面那样的做法。
不幸的是,由于像刚才所说的那样,for并没有每次都单独创建scope,因此closure全部引用到了同一个变量上,导致所有的属性值取出来都是最后一个属性了。看到这样诡异的bug,如果是在C/C++里面,我肯定要怀疑是内存或者指针的问题了。不过想了半天才终于恍然大悟!不过Python里面没有Ruby那么方便的each可以用,lambda用起来也很鸡肋,所以最后通过定义一个局部的函数来解决了:
defproxy_prop(name): setattr(proxy,'get_%s'%prop,lambda:self.get_prop(name) forpropinpublic_props: proxy_prop(prop)
最后,还要多嘴一句,对于之前Ruby那个例子,如果把each和for的执行顺序颠倒过来,会得到不同的结果:
arr=['a','b','c'] h1=Hash.new h2=Hash.new foreinarr h2[e]=lambda{e+'!'} end arr.each{|e| h1[e]=lambda{e+'!'} } h1['a'].call#=>'c!' h2['a'].call#=>'c!'
现在两个都是'c!'了!这是因为Ruby1.8的实现里面block的参数可以对局部变量或者全局变量之类的任何东西进行赋值,而不是通常意义上的一个lambda函数的参数那么简单。由于前面的for语句在当前作用域创建了一个e作为局部变量,因此each就直接对这个局部变量进行赋值了,这样,每次引用到的又变成了同一个东西,导致了一个隐秘的Bug!
值得庆幸的是,block的这个“特性”在Ruby1.9中已经被去除了,block的参数只能是正常参数,所以就不再存在这样的问题了。希望1.9尽快普及吧!