Python class 使用字典理解的变量赋值

Python class variable assignment using dictionary comprehension

在 class 定义期间,定义为字典的 class 变量用于构造第二个字典 class 变量,该子集从第一个字典中缩减,例如这个:

class C(object):
    ALL_ITEMS = dict(a='A', b='B', c='C', d='D', e='E')
    SUBSET_X = {k: v for k, v in ALL_ITEMS.items() if k in ('a', 'b', 'd')}  # (this works)
    SUBSET_Y = {k: ALL_ITEMS[k] for k in ('a', 'b', 'd')}  # (this fails)

非常简单的东西,但是执行这段代码的最终效果让我很惊讶。我的第一个方法是第 4 行的代码,但我不得不求助于第 3 行的解决方案。我显然没有掌握字典理解范围规则的一些微妙之处。

具体来说,失败案例中引发的错误是:

File "goofy.py", line 4, in <dictcomp>
   SUBSET_Y = {k: ALL_ITEMS.get(k) for k in ('a', 'b', 'd')}
NameError: name 'ALL_ITEMS' is not defined

由于几个不同的原因,这个错误的性质让我感到困惑:

  1. SUBSET_Y 的赋值是一个格式正确的字典理解,并引用了一个应该在范围内且可访问的符号。
  2. 在随后的情况下(对 SUBSET_X 的赋值),这也是一个字典理解,符号 ALL_ITEMS 是完全明确定义和可访问的。因此,在失败案例中引发的异常是 NameError 这一事实似乎显然是错误的。 (或者充其量是误导。)
  3. 为什么 items()__getitem__get() 的范围规则不同? (在失败情况下,将 ALL_ITEMS[k] 替换为 ALL_ITEMS.get(k) 会发生相同的异常。)

(即使作为 Python 十多年的开发人员,我以前从未 运行 陷入这种失败,这要么意味着我很幸运,要么过着安逸的生活 :^ )

同样的故障出现在各种3.6.x CPython版本和2.7.x版本中。

编辑:不,这不是上一个问题的重复。这与列表推导有关,即使将相同的解释投射到字典推导上,也无法解释我引用的两个案例之间的区别。而且,这不是 Python 3-only 现象。

有一个小细节可以解释为什么第一个版本有效但第二个版本失败。第二个版本失败的原因与 this question 中给出的原因相同,即所有理解构造(在 Python 3 中,在 Python 2 中,列表理解的实现方式不同)创建发生所有本地名称绑定的函数范围。但是,class 作用域中的名称对于 class 作用域内定义的函数是不可访问的。这就是为什么您必须使用 self.MY_CLASS_VARMyClass.MY_CLASS_VAR 从方法访问 class 变量的原因。

您的第一个案例恰好有效的原因很微妙。根据 language reference

The comprehension consists of a single expression followed by at least one for clause and zero or more for or if clauses. In this case, the elements of the new container are those that would be produced by considering each of the for or if clauses a block, nesting from left to right, and evaluating the expression to produce an element each time the innermost block is reached.

However, aside from the iterable expression in the leftmost for clause, the comprehension is executed in a separate implicitly nested scope. This ensures that names assigned to in the target list don’t “leak” into the enclosing scope.

The iterable expression in the leftmost for clause is evaluated directly in the enclosing scope and then passed as an argument to the implictly nested scope.

因此,在第一种情况下,ALL_ITEMS.items() 位于最左侧的 for 子句中,因此 直接在封闭范围内计算 ,在本例中,class 作用域,所以它很高兴地找到了 ALL_ITEMS 名称。