BLOG
Enjoy when you can, and endure when you must.
JUN 01, 2015/Python
别被全局变量坑了:lambda 在列表解析中使用的陷阱一例

我们从一个很有意思的例子开始。对于一个列表 [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 %}
    <td>{{ o.get_status_display }}</td>
{% 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 会始终指向最后一个迭代出来的对象。

因此,我们必须时刻关注细节,注重基础和原理,这样才能在代码实现过程中规避掉可能存在的风险。

COMMENTS
14/07From Danny

@daltonxiong: 我们之后是在自己工程内针对 field_display 统一重新封装了一次,也绕过了他原生的 get_field_display() 方法

13/07From daltonxiong

get_field_display 请问最后你是怎么解决的啊?

13/07From daltonxiong

我也遇到了这个问题 研究了半天还是解决不了

LEAVE COMMNT