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


Posted in Python onJune 04, 2014

虽然这个问题我是在 Python 里遇到的,但是用 Ruby 解释起来比较容易一些。在 Ruby 里,遍历一个数组可以有很多种方法,最常用的两种无非是 for 和 each:

arr = ['a', 'b', 'c']arr.each { |e|
  puts e
}
for e in arr
  puts e
end

通常我比较喜欢后者,似乎因为写起来比较好看,不过从效率上来说前者应该会稍微快一点,因为后者实际上是在遍历的过程中对每个元素都调用一个 lambda 函数来做的,虽然一般情况下并不明显,不过设置上下文并调用函数确实是有开销的,特别是在动态语言里面(不考虑 JIT 内联优化的话)。不过这次的问题并不是性能。然而确实跟“ each 对每个元素都会新建一个 scope 而 for 则不是”有关。

看下面一段代码:

arr = ['a', 'b', 'c']
h1 = Hash.new
h2 = Hash.newarr.each { |e|
  h1[e] = lambda { e+'!'}
}
for e in arr
  h2[e] = lambda { e+'!' }
end
h1['a'].call # => ?
h2['a'].call # => ?

两个 call 分别会得到什么?应该已经猜到了吧?分别是 'a!' 和 'c!' ,后者之所以是 'c!' 是因为 for 并没有在循环的每一步都重新创建一个 scope ,因此三个 lambda 的 closure 引用到了同一个变量,而这个变量在最后一次被赋值为 'c' ,所以导致了这样的后果。

问题其实出自我在用 Python 写的一个小程序中的一段,代码类似于这样:

for prop in public_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 用起来也很鸡肋,所以最后通过定义一个局部的函数来解决了:

def proxy_prop(name):
    setattr(proxy, 'get_%s'%prop, lambda: self.get_prop(name)
for prop in public_props:
    proxy_prop(prop)

最后,还要多嘴一句,对于之前 Ruby 那个例子,如果把 each 和 for 的执行顺序颠倒过来,会得到不同的结果:
arr = ['a', 'b', 'c']
h1 = Hash.new
h2 = Hash.newfor e in arr
  h2[e] = lambda { e+'!' }
end
arr.each { |e|
  h1[e] = lambda { e+'!'}
}
h1['a'].call # => 'c!'
h2['a'].call # => 'c!'

现在两个都是 'c!' 了!这是因为 Ruby 1.8 的实现里面 block 的参数可以对局部变量或者全局变量之类的任何东西进行赋值,而不是通常意义上的一个 lambda 函数的参数那么简单。由于前面的 for 语句在当前作用域创建了一个 e 作为局部变量,因此 each 就直接对这个局部变量进行赋值了,这样,每次引用到的又变成了同一个东西,导致了一个隐秘的 Bug !

值得庆幸的是,block 的这个“特性”在 Ruby 1.9 中已经被去除了,block 的参数只能是正常参数,所以就不再存在这样的问题了。希望 1.9 尽快普及吧!

Python 相关文章推荐
纯Python开发的nosql数据库CodernityDB介绍和使用实例
Oct 23 Python
python使用htmllib分析网页内容的方法
May 08 Python
Django imgareaselect手动剪切头像实现方法
May 26 Python
简单了解Python下用于监视文件系统的pyinotify包
Nov 13 Python
python正则表达式爬取猫眼电影top100
Feb 24 Python
对python中的logger模块全面讲解
Apr 28 Python
Flask框架通过Flask_login实现用户登录功能示例
Jul 17 Python
pycharm 批量修改变量名称的方法
Aug 01 Python
详解基于python的多张不同宽高图片拼接成大图
Sep 26 Python
python json load json 数据后出现乱序的解决方案
Feb 27 Python
在PyTorch中使用标签平滑正则化的问题
Apr 03 Python
Python基础之hashlib模块详解
May 06 Python
python控制台英汉汉英电子词典
Apr 23 #Python
测试、预发布后用python检测网页是否有日常链接
Jun 03 #Python
Python中的CURL PycURL使用例子
Jun 01 #Python
Python实现多线程下载文件的代码实例
Jun 01 #Python
python使用在线API查询IP对应的地理位置信息实例
Jun 01 #Python
pip 错误unused-command-line-argument-hard-error-in-future解决办法
Jun 01 #Python
2款Python内存检测工具介绍和使用方法
Jun 01 #Python
You might like
PHP 中的面向对象编程:通向大型 PHP 工程的办法
2006/12/03 PHP
PHP实现获取域名的方法小结
2014/11/05 PHP
完美解决thinkphp验证码出错无法显示的方法
2014/12/09 PHP
PHP请求Socket接口测试实例
2016/08/12 PHP
Yii 2中的load()和save()示例详解
2017/08/03 PHP
PDO::rollBack讲解
2019/01/29 PHP
php高性能日志系统 seaslog 的安装与使用方法分析
2020/02/29 PHP
QQ登录简单实现代码
2021/03/09 Javascript
JavaScript Tips 使用DocumentFragment加快DOM渲染速度
2010/06/28 Javascript
$.format,jquery.format 使用说明
2011/07/13 Javascript
JavaScript实现动态创建CSS样式规则方案
2014/09/06 Javascript
ECMAScript6函数默认参数
2015/06/12 Javascript
jsTree使用记录实例
2016/12/01 Javascript
Vuejs 组件——props数据传递的实例代码
2017/03/07 Javascript
浅析bootstrap原理及优缺点
2017/03/19 Javascript
angular4自定义组件详解
2017/09/28 Javascript
详谈构造函数加括号与不加括号的区别
2017/10/26 Javascript
基于JSONP原理解析(推荐)
2017/12/04 Javascript
详解无限滚动插件vue-infinite-scroll源码解析
2019/05/12 Javascript
JavaScript装饰者模式原理与用法实例详解
2020/03/09 Javascript
python 示例分享---逻辑推理编程解决八皇后
2014/07/20 Python
基于Python实现的百度贴吧网络爬虫实例
2015/04/17 Python
Tensorflow简单验证码识别应用
2017/05/25 Python
python中数组和矩阵乘法及使用总结(推荐)
2019/05/18 Python
python3 mmh3安装及使用方法
2019/10/09 Python
用python拟合等角螺线的实现示例
2019/12/27 Python
Pytorch 多维数组运算过程的索引处理方式
2019/12/27 Python
CSS3实现歌词进度文字颜色填充变化动态效果的思路详解
2020/06/02 HTML / CSS
shell的种类有哪些
2015/04/15 面试题
金融学专科生自我鉴定
2014/02/21 职场文书
项目经理任命书范本
2014/06/05 职场文书
2015年实习单位评语
2015/03/25 职场文书
详解mysql三值逻辑与NULL
2021/05/19 MySQL
Python中else的三种使用场景
2021/06/16 Python
centos8安装MongoDB的详细过程
2021/10/24 MongoDB
Redis 报错 error:NOAUTH Authentication required
2022/05/15 Redis