嵌套列表理解范围

sn6*_*6uv 23 python scope cpython python-3.x

解释我的问题的最好方法是举个例子:

example.py:

class A(object):
    integers = [1, 2, 3]
    singles = [i for i in integers]

class B(object):
    integers = [1, 2, 3]
    pairs = [(i, j) for i in integers for j in integers]
Run Code Online (Sandbox Code Playgroud)

当我在python 2下运行它时工作正常,但在python 3下我得到一个NameErrorfor class B(但不是class A):

$ python example.py
Traceback (most recent call last):
  File "example.py", line 6, in <module>
    class B(object):
  File "example.py", line 8, in B
    pairs = [(i, j) for i in integers for j in integers]
  File "example.py", line 8, in <listcomp>
    pairs = [(i, j) for i in integers for j in integers]
NameError: global name 'integers' is not defined
Run Code Online (Sandbox Code Playgroud)

为什么只有类B提出一个NameError,为什么只在Python 3下?

Blc*_*ght 23

类范围在Python 3中有点奇怪,但它有充分的理由.

在Python 2中,迭代变量(i以及j在您的示例中)从列表推导中泄露出来并将包含在外部范围中.这是因为它们是在Python 2的设计早期开发的,它们基于显式循环.作为一个如何出乎意料的例子,检查Python 2中没有出错的值B.iB.j!

在Python 3中,列表推导已更改以防止此泄漏.它们现在使用一个函数(具有自己的作用域)来实现,该函数被调用以生成列表值.这使得它们与生成器表达式的工作方式相同,生成器表达式一直是封面下的函数.

这样做的结果是,在类中,列表推导通常看不到任何类变量.这与无法直接查看类变量的方法(仅限self于显式类名称)并行.例如,调用下面的类中的方法将给出NameError您在列表推导中看到的相同异常:

class Foo:
    classvar = "bar"
    def blah(self):
        print(classvar) # raises "NameError: global name 'classvar' is not defined"
Run Code Online (Sandbox Code Playgroud)

但是有一个例外.由for列表推导的第一个子句迭代的序列在内部函数之外进行计算.这就是为什么你的A类在Python 3中工作的原因.这样做可以使生成器立即捕获不可迭代的对象(而不是仅next在它们被调用并且它们的代码运行时).

但它对于for课堂两级理解中的内部子句不起作用B.

如果您使用dis模块反汇编某些创建列表推导的函数,您可以看到区别:

def f(lst):
    return [i for i in lst]

def g(lst):
    return [(i, j) for i in lst for j in lst]
Run Code Online (Sandbox Code Playgroud)

这是反汇编f:

>>> dis.dis(f)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x0000000003CCA1E0, file "<pyshell#374>", line 2>) 
              3 LOAD_CONST               2 ('f.<locals>.<listcomp>') 
              6 MAKE_FUNCTION            0 
              9 LOAD_FAST                0 (lst) 
             12 GET_ITER             
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             16 RETURN_VALUE       
Run Code Online (Sandbox Code Playgroud)

前三行显示f加载预编译的代码块并从中创建一个函数(它命名f.<locals>.<listcomp>).这是用于制作列表的函数.

接下来的两行显示lst正在加载的变量以及从中生成的迭代器.这发生在f范围内,而不是内部函数.然后<listcomp>使用该迭代器作为其参数调用该函数.

这与班级相当A.它从类变量中获取迭代器integers,就像您可以在新成员的定义中使用对前一个类成员的其他类型的引用一样.

现在,比较反汇编g,它通过迭代两次相同的列表来生成对:

>>> dis.dis(g)
  2           0 LOAD_CLOSURE             0 (lst) 
              3 BUILD_TUPLE              1 
              6 LOAD_CONST               1 (<code object <listcomp> at 0x0000000003CCA810, file "<pyshell#377>", line 2>) 
              9 LOAD_CONST               2 ('g.<locals>.<listcomp>') 
             12 MAKE_CLOSURE             0 
             15 LOAD_DEREF               0 (lst) 
             18 GET_ITER             
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             22 RETURN_VALUE         
Run Code Online (Sandbox Code Playgroud)

这次,它使用代码对象而不是基本函数构建闭包.闭包是一个带有一些"自由"变量的函数,这些变量引用封闭范围内的东西.对于<listcomp>函数g,这很好用,因为它的范围是正常的.但是,当你尝试在类B中使用相同类型的理解时,闭包失败,因为类不允许它们包含的函数以这种方式看到它们的作用域(如Foo上面的类所示).

值得注意的是,不仅内部序列值会导致此问题.如在BrenBarn在评论中链接的上一个问题一样,如果在列表推导中的其他位置引用类变量,则会遇到相同的问题:

class C:
    num = 5
    products = [i * num for i in range(10)] # raises a NameError about num
Run Code Online (Sandbox Code Playgroud)

但是,您不会从多级列表推导中得到错误,其中内部for(或if)子句仅引用前一个循环的结果.这是因为这些值不是闭包的一部分,只是<listcomp>函数范围内的局部变量.

class D:
    nested = [[1, 2, 3], [4, 5, 6]]
    flattened = [item for inner in nested for item in inner] # works!
Run Code Online (Sandbox Code Playgroud)

就像我说的,课程范围有点奇怪.

  • @ sn6uv:因为在理解的第一个“ for”子句中迭代的对象是在外部范围内评估的,而不是在理解所创建的范围内评估的。如果您认为这很奇怪,那是对的。此行为是设计决策的结果,该决策旨在帮助在生成器表达式中更早地检测到错误。 (2认同)