zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Python和Ruby中each循环引用变量问题(一个隐秘BUG?)

PythonBUG循环ruby变量 问题 一个 引用
2023-06-13 09:15:27 时间

虽然这个问题我是在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尽快普及吧!