为什么一个类变量在列表推导中未定义而另一个已定义?

25 投票
2 回答
6333 浏览
提问于 2025-04-18 00:10

我刚刚读了这个问题的答案:在类定义中通过列表推导访问类变量

这让我明白了为什么以下代码会出现 NameError: name 'x' is not defined 的错误:

class A:
    x = 1
    data = [0, 1, 2, 3]
    new_data = [i + x for i in data]
    print(new_data)

这个 NameError 错误出现是因为 x 在列表推导的特殊范围内没有被定义。但是我不明白为什么下面的代码没有任何错误。

class A:
    x = 1
    data = [0, 1, 2, 3]
    new_data = [i for i in data]
    print(new_data)

我得到了输出 [0, 1, 2, 3]。但我本来以为会出现这个错误:NameError: name 'data' is not defined,因为我以为就像之前的例子一样,x 在列表推导的范围内没有定义,类似的,data 在列表推导的范围内也不会被定义。

你能帮我理解为什么 x 在列表推导的范围内没有定义,但 data 却有吗?

相关问题:

2 个回答

-1

这个关于 dis.dis 的回答很有趣,但实际上并没有解释为什么会这样。这里有一个来自类似错误的解释:

如果在代码块的任何地方发生了名称绑定操作,那么在这个代码块内使用这个名称的所有地方都会被视为对当前代码块的引用。这可能会导致错误,特别是当在绑定之前就使用了这个名称时。这个规则比较微妙。Python没有声明机制,允许在代码块的任何地方进行名称绑定操作。代码块的局部变量可以通过扫描整个代码块的文本来确定。

简单来说:data 不能引用 x,因为在那个时刻代码块还没有绑定。也就是说,无法通过 x 或者 A.x 来引用 x

来源:python 文档

20

data 是列表推导式的 来源;它是传递给创建的嵌套作用域的唯一参数。

在列表推导式中,除了最左边的 for 循环使用的可迭代对象之外,其他所有内容都是在一个独立的作用域中运行的(基本上就像一个函数)。你可以在字节码中看到这一点:

>>> def foo():
...     return [i for i in data]
... 
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x105390390, file "<stdin>", line 2>)
              3 LOAD_CONST               2 ('foo.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              0 (data)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 RETURN_VALUE

<listcomp> 代码对象就像函数一样被调用,iter(data) 被作为参数传入(CALL_FUNCTION 被执行时有一个位置参数,也就是 GET_ITER 的结果)。

<listcomp> 代码对象会查找那个唯一的参数:

>>> dis.dis(foo.__code__.co_consts[1])
  2           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                12 (to 21)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 LIST_APPEND              2
             18 JUMP_ABSOLUTE            6
        >>   21 RETURN_VALUE

LOAD_FAST 调用指的是传入的第一个也是唯一的参数;这里没有名字是因为从来没有函数定义给它命名。

在列表推导式(或者集合推导式、字典推导式、生成器表达式等)中使用的任何额外名称,都是局部变量、闭包或全局变量,而不是参数。

如果你回到我对那个问题的 回答,可以找到标题为 小例外;或者,为什么某一部分可能仍然有效 的部分;我在那里面试图涵盖这个具体点:

无论 Python 版本如何,推导式或生成器表达式中有一部分会在周围的作用域中执行。那就是外层可迭代对象的表达式。

撰写回答