我们从一个很有意思的例子开始。对于一个列表 [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 会始终指向最后一个迭代出来的对象。
因此,我们必须时刻关注细节,注重基础和原理,这样才能在代码实现过程中规避掉可能存在的风险。