IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    别被全局变量坑了:lambda 在列表解析中使用的陷阱一例

    博客 - DannySite发表于 2015-05-31 16:28:28
    love 0
    我们从一个很有意思的例子开始。对于一个列表 [1, 2, 3, 4, 5],如果要将其中的每个元素都乘以2,利用较为 Pythonic 的方式的话,一般会这么做: >>> a [1, 2, 3, 4, 5] >>> b = [i * 2 for i in a] >>> for item in b: print(item, end=' ') 2 4 6 8 10 这的确是期望中的结果,但如果卖个关子(俗称装个逼)这样玩呢: >>> a [1, 2, 3, 4, 5] >>> c = [lambda: i * 2 for i in a] >>> for item in c: print(item(), end=' ') 10 10 10 10 10 就出现了一个诡异的现象,怎么列表中的每个元素都成 10 了! 让我们将循环和 lambda 函数展开看看具体的情况: >>> for i in a: def f(): return i * 2 c.append(f) >>> for item in c: print(item(), end=' ') 10 10 10 10 10 问题就出在函数 f 中,也就是之前的 lambda 上。我们都知道 Python 只有在函数实际执行的时候才会去检查变量,而这里的变量 i 又不在函数作用域内,因此是到上层作用域搜索,这里可以理解成全局变量了。而问题恰恰就出在此,i 已经在 for 迭代中变为了列表 a 中的最后一个元素 10,由此导致了最后的结果。 不要小看这个细节,最近我在使用 mongoengine 时还真是栽进了这个坑里。get_field_display 是针对具有 choices 的 Field 非常方便的方法: {% for o in object_list %} {{ o.get_status_display }} {% endfor %} 结果发现得到全篇一样的数据,这让我瞬间意识到了我遭遇了刚才提到的问题。但我自己的代码只是使用,难道是框架自身有此问题?经过分析,看来应该是的,在 mongoengine\base\document.py 中的 BaseDocument 类有如下关键代码: class BaseDocument(object): ... _dynamic = False ... def __init__(self, *args, **values): ... # Set any get_fieldname_display methods self.__set_field_display() ... def __set_field_display(self): """Dynamically set the display value for a field with choices""" for attr_name, field in self._fields.items(): if field.choices: if self._dynamic: obj = self else: obj = type(self) setattr(obj, 'get_%s_display' % attr_name, partial(self.__get_field_display, field=field)) def __get_field_display(self, field): """Returns the display value for a choice field""" value = getattr(self, field.name) if field.choices and isinstance(field.choices[0], (list, tuple)): return dict(field.choices).get(value, value) return value __set_field_display() 方法乍一看写得很工整没有任何问题,但其中的这一段代码引起了我的注意: if self._dynamic: obj = self else: obj = type(self) _dynamic 在多数情况下都是 False,也就是基本都会走到 else 分支。那就是说,obj 是 BaseDocument 自身。接下来的 setattr 则毋庸置疑的是针对 BaseDocument,因此相当于是在设置全局变量。再细看一下给 __get_field_display() 传入的两个参数,field 没有什么疑问,self 则很关键,__set_field_display() 是在 __init__() 中调用的,这里传入的 self 是实例化的 BaseDocument 对象,而这里对属性的设置又是全局性的,由此导致了刚才的那种问题。在 for 循环中去实际调用的使用,__get_field_display() 中的 self 会始终指向最后一个迭代出来的对象。 因此,我们必须时刻关注细节,注重基础和原理,这样才能在代码实现过程中规避掉可能存在的风险。


沪ICP备19023445号-2号
友情链接