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

tre*_*ant 2 python dictionary class scoping

在类定义期间,定义为字典的类变量用于构建第二个字典类变量,这是从第一个缩减而来的子集,如下所示:

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)
Run Code Online (Sandbox Code Playgroud)

非常简单的东西,但执行这段代码的最终效果让我感到非常惊讶。我的第一种方法是第 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
Run Code Online (Sandbox Code Playgroud)

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

  1. 分配给SUBSET_Y是一个格式良好的字典理解,并引用一个应该在范围内且可访问的符号。
  2. 在后一种情况下(对 的赋值SUBSET_X),这也是一个字典理解,该符号ALL_ITEMS是完全明确且可访问的。因此,NameError在失败的情况下引发异常的事实似乎显然是错误的。(或者充其量是误导。)
  3. 为什么items()vs.__getitem__或的范围规则会有所不同get()?(同样的异常发生在失败的情况下替换ALL_ITEMS[k]ALL_ITEMS.get(k)。)

(即使是十多年的 Python 开发人员,我以前从未遇到过这种失败,这要么意味着我很幸运,要么过着隐蔽的生活:^)

同样的失败发生在各种 3.6.x CPython 版本以及 2.7.x 版本中。

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

jua*_*aga 6

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

您的第一个案例确实起作用的原因很微妙。根据语言参考

推导式由单个表达式后跟至少一个 for 子句和零个或多个 for 或 if 子句组成。在这种情况下,新容器的元素是通过将每个 for 或 if 子句视为一个块,从左到右嵌套,并在每次到达最里面的块时计算表达式以生成一个元素来生成的元素。

然而,除了最左边的 for 子句中的可迭代表达式之外,推导式在单独的隐式嵌套范围内执行。这确保分配给目标列表中的名称不会“泄漏”到封闭范围中。

最左边的 for 子句中的可迭代表达式在封闭范围内直接计算,然后作为参数传递给隐式嵌套范围。

所以,在第一种情况下,ALL_ITEMS.items()在最左边的 for 子句中,所以它直接在封闭范围内评估,在这种情况下,类范围,所以它很高兴找到ALL_ITEMS名称。