如何从类定义中的列表理解中访问其他类变量?以下代码在python2中有效,但在python3中无效:

class Foo:
    x = 5
    y = [x for i in range(1)]

Python 3.2给出了以下错误:

NameError: global name 'x' is not defined

尝试Foo。X也不行。关于如何在python3中做到这一点,有什么想法吗?

一个稍微复杂一点的激励例子:

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

在这个例子中,apply()本来是一个不错的解决方案,但不幸的是,它从Python 3中被删除了。


类作用域和列表、集或字典推导式以及生成器表达式不能混合。

为什么;或者是官方的说法

在Python 3中,列表推导式被赋予了自己的适当作用域(局部命名空间),以防止它们的局部变量溢出到周围的作用域(参见列表推导式即使在推导作用域之后也会重新绑定名称)。这样对吗?)当在模块或函数中使用这样的列表理解时,这很好,但在类中,作用域有点,嗯,奇怪。

这在pep 227中有记录:

类作用域中的名称不可访问。名称在 最里面的封闭函数作用域。如果一个类定义 在一个嵌套作用域链中发生时,解析过程将跳过 类定义。

在类复合语句文档中:

然后,类的套件在一个新的执行框架中执行(请参阅命名和绑定部分),使用新创建的本地名称空间和原始的全局名称空间。(通常,该套件只包含函数定义。)当类的套件完成执行时,它的执行框架将被丢弃,但它的本地命名空间将被保存。然后使用基类的继承列表和属性字典保存的本地名称空间创建类对象。

我特别强调;执行框架是临时作用域。

因为作用域被重新定义为类对象的属性,允许它被用作非局部作用域也会导致未定义的行为;如果一个类方法引用x作为一个嵌套的作用域变量,然后操纵Foo会发生什么。X也是,比如说?更重要的是,这对Foo的子类意味着什么?Python必须以不同的方式对待类作用域,因为它与函数作用域非常不同。

最后,但绝对不是最不重要的,在执行模型文档中链接的命名和绑定部分显式地提到了类作用域:

类块中定义的名称的作用域仅限于类块;它不扩展到方法的代码块——这包括推导式和生成器表达式,因为它们是使用函数作用域实现的。这意味着以下将失败: 甲级: A = 42 B = list(a + I for I in range(10))

So, to summarize: you cannot access the class scope from functions, list comprehensions or generator expressions enclosed in that scope; they act as if that scope does not exist. In Python 2, list comprehensions were implemented using a shortcut, but in Python 3 they got their own function scope (as they should have had all along) and thus your example breaks. Other comprehension types have their own scope regardless of Python version, so a similar example with a set or dict comprehension would break in Python 2.

# Same error, in Python 2 or 3
y = {x: x for i in range(1)}

(小)例外;或者,为什么有一部分还能工作

无论Python版本如何,理解式或生成器表达式都有一部分在周围的作用域中执行。这将是最外层可迭代对象的表达式。在你的例子中,它是范围(1):

y = [x for i in range(1)]
#               ^^^^^^^^

因此,在表达式中使用x不会抛出错误:

# Runs fine
y = [i for i in range(x)]

这只适用于最外层的迭代对象;如果一个理解式有多个for子句,则内部for子句的可迭代对象将在理解式的作用域内计算:

# NameError
y = [i for i in range(1) for j in range(x)]
#      ^^^^^^^^^^^^^^^^^ -----------------
#      outer loop        inner, nested loop

做出此设计决策是为了在genexp创建时而不是迭代时抛出错误,当创建生成器表达式的最外层可迭代对象时抛出错误,或者当最外层可迭代对象不是可迭代对象时抛出错误。理解式为了一致性共享此行为。

在引擎盖下寻找;或者,比你想要的更详细

您可以使用dis模块看到所有这些操作。在下面的示例中,我使用Python 3.3,因为它添加了限定名称,可以整齐地标识我们想要检查的代码对象。生成的字节码在其他方面的功能与Python 3.2相同。

为了创建一个类,Python实际上采用了组成类主体的整个套件(因此比class <name>: line更深缩进的所有内容),并像执行函数一样执行:

>>> import dis
>>> def foo():
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo)
  2           0 LOAD_BUILD_CLASS     
              1 LOAD_CONST               1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>) 
              4 LOAD_CONST               2 ('Foo') 
              7 MAKE_FUNCTION            0 
             10 LOAD_CONST               2 ('Foo') 
             13 CALL_FUNCTION            2 (2 positional, 0 keyword pair) 
             16 STORE_FAST               0 (Foo) 

  5          19 LOAD_FAST                0 (Foo) 
             22 RETURN_VALUE         

第一个LOAD_CONST加载Foo类主体的代码对象,然后将其转换为函数,并调用它。然后,该调用的结果用于创建类的命名空间__dict__。到目前为止一切顺利。

这里需要注意的是,字节码包含一个嵌套的代码对象;在Python中,类定义、函数、推导式和生成器都表示为代码对象,这些代码对象不仅包含字节码,还包含表示局部变量、常量、全局变量和嵌套作用域变量的结构。编译后的字节码引用这些结构,python解释器知道如何访问给定的字节码。

这里要记住的重要一点是,Python在编译时创建这些结构;类套件是一个已经编译的代码对象(<code对象Foo at 0x10a436030,文件"<stdin>",第2行>)。

让我们检查一下创建类主体本身的代码对象;代码对象有一个co_consts结构:

>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (__locals__) 
              3 STORE_LOCALS         
              4 LOAD_NAME                0 (__name__) 
              7 STORE_NAME               1 (__module__) 
             10 LOAD_CONST               0 ('foo.<locals>.Foo') 
             13 STORE_NAME               2 (__qualname__) 

  3          16 LOAD_CONST               1 (5) 
             19 STORE_NAME               3 (x) 

  4          22 LOAD_CONST               2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>) 
             25 LOAD_CONST               3 ('foo.<locals>.Foo.<listcomp>') 
             28 MAKE_FUNCTION            0 
             31 LOAD_NAME                4 (range) 
             34 LOAD_CONST               4 (1) 
             37 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             40 GET_ITER             
             41 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             44 STORE_NAME               5 (y) 
             47 LOAD_CONST               5 (None) 
             50 RETURN_VALUE         

The above bytecode creates the class body. The function is executed and the resulting locals() namespace, containing x and y is used to create the class (except that it doesn't work because x isn't defined as a global). Note that after storing 5 in x, it loads another code object; that's the list comprehension; it is wrapped in a function object just like the class body was; the created function takes a positional argument, the range(1) iterable to use for its looping code, cast to an iterator. As shown in the bytecode, range(1) is evaluated in the class scope.

从这里你可以看到,函数或生成器的代码对象与理解的代码对象之间的唯一区别是,后者在执行父代码对象时立即执行;字节码只是动态地创建一个函数,并在几个小步骤中执行它。

Python 2。x使用内联字节码代替,下面是Python 2.7的输出:

  2           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  3           6 LOAD_CONST               0 (5)
              9 STORE_NAME               2 (x)

  4          12 BUILD_LIST               0
             15 LOAD_NAME                3 (range)
             18 LOAD_CONST               1 (1)
             21 CALL_FUNCTION            1
             24 GET_ITER            
        >>   25 FOR_ITER                12 (to 40)
             28 STORE_NAME               4 (i)
             31 LOAD_NAME                2 (x)
             34 LIST_APPEND              2
             37 JUMP_ABSOLUTE           25
        >>   40 STORE_NAME               5 (y)
             43 LOAD_LOCALS         
             44 RETURN_VALUE        

没有加载代码对象,而是内联运行FOR_ITER循环。在python3中。X时,列表生成器被赋予了自己的适当代码对象,这意味着它有自己的作用域。

然而,当解释器第一次加载模块或脚本时,推导式与其余的python源代码一起编译,编译器不认为类套件是有效的作用域。列表推导式中的任何引用变量都必须递归地在类定义周围的范围内查找。如果编译器没有找到该变量,则将其标记为全局变量。列表理解代码对象的反汇编显示x确实是作为全局变量加载的:

>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
  4           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_GLOBAL              0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

这个字节码块加载传入的第一个参数(range(1)迭代器),就像Python 2。x版本使用FOR_ITER循环它并创建它的输出。

如果我们在foo函数中定义x, x将是一个单元格变量(单元格指嵌套的作用域):

>>> def foo():
...     x = 2
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
  5           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_DEREF               0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

LOAD_DEREF将间接地从代码对象单元格对象中加载x:

>>> foo.__code__.co_cellvars               # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars  # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars  # Refers to `x` in foo
('x',)
>>> foo().y
[2]

实际的引用是从当前帧数据结构中查找值,这些数据结构是由函数对象的.__closure__属性初始化的。因为为理解代码对象创建的函数再次被丢弃,所以我们不能检查该函数的闭包。要查看闭包的运行情况,我们必须检查一个嵌套函数:

>>> def spam(x):
...     def eggs():
...         return x
...     return eggs
... 
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5

所以,总结一下:

List comprehensions get their own code objects in Python 3, and there is no difference between code objects for functions, generators or comprehensions; comprehension code objects are wrapped in a temporary function object and called immediately. Code objects are created at compile time, and any non-local variables are marked as either global or as free variables, based on the nested scopes of the code. The class body is not considered a scope for looking up those variables. When executing the code, Python has only to look into the globals, or the closure of the currently executing object. Since the compiler didn't include the class body as a scope, the temporary function namespace is not considered.

一个解决方案;或者,该怎么做

如果你想为x变量创建一个显式作用域,就像在函数中一样,你可以使用类作用域变量来进行列表推导:

>>> class Foo:
...     x = 5
...     def y(x):
...         return [x for i in range(1)]
...     y = y(x)
... 
>>> Foo.y
[5]

“临时”y函数可以直接调用;我们用它的返回值替换它。在解析x时考虑其作用域:

>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)

当然,阅读您的代码的人会对此有点挠头;你可能想要在那里放一个大的评论,解释你为什么这么做。

最好的解决方法是使用__init__来创建一个实例变量:

def __init__(self):
    self.y = [self.x for i in range(1)]

避免所有的挠头和解释自己的问题。对于您自己的具体示例,我甚至不会将namedtuple存储在类中;要么直接使用输出(完全不存储生成的类),要么使用全局:

from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]

在我看来,这是Python 3的一个缺陷。我希望他们能改变。

旧方法(适用于2.7,抛出NameError: name 'x'未在3+中定义):

class A:
    x = 4
    y = [x+i for i in range(1)]

注意:简单地用A.x限定它的作用域并不能解决这个问题

新方法(适用于3岁以上):

class A:
    x = 4
    y = (lambda x=x: [x+i for i in range(1)])()

因为语法太丑了,我通常只在构造函数中初始化所有的类变量


公认的答案提供了很好的信息,但这里似乎还有一些其他问题——列表理解和生成器表达式之间的差异。我玩了一个演示:

class Foo:

    # A class-level variable.
    X = 10

    # I can use that variable to define another class-level variable.
    Y = sum((X, X))

    # Works in Python 2, but not 3.
    # In Python 3, list comprehensions were given their own scope.
    try:
        Z1 = sum([X for _ in range(3)])
    except NameError:
        Z1 = None

    # Fails in both.
    # Apparently, generator expressions (that's what the entire argument
    # to sum() is) did have their own scope even in Python 2.
    try:
        Z2 = sum(X for _ in range(3))
    except NameError:
        Z2 = None

    # Workaround: put the computation in lambda or def.
    compute_z3 = lambda val: sum(val for _ in range(3))

    # Then use that function.
    Z3 = compute_z3(X)

    # Also worth noting: here I can refer to XS in the for-part of the
    # generator expression (Z4 works), but I cannot refer to XS in the
    # inner-part of the generator expression (Z5 fails).
    XS = [15, 15, 15, 15]
    Z4 = sum(val for val in XS)
    try:
        Z5 = sum(XS[i] for i in range(len(XS)))
    except NameError:
        Z5 = None

print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)

这是Python中的一个bug。推导式被宣传为等同于for循环,但在类中并非如此。至少在Python 3.6.6之前,在类中使用的理解型中,只有一个来自理解型外部的变量可以在理解型内部访问,并且它必须用作最外层的迭代器。在函数中,此范围限制不适用。

为了说明为什么这是一个错误,让我们回到最初的示例。这个操作失败:

class Foo:
    x = 5
    y = [x for i in range(1)]

但这是可行的:

def Foo():
    x = 5
    y = [x for i in range(1)]

在参考指南的本节末尾说明了限制。


由于最外层的迭代器是在周围范围内计算的,我们可以将zip与itertools一起使用。重复将依赖项转移到理解范围:

import itertools as it

class Foo:
    x = 5
    y = [j for i, j in zip(range(3), it.repeat(x))]

还可以在推导式中使用嵌套for循环,并在最外层的可迭代对象中包含依赖项:

class Foo:
    x = 5
    y = [j for j in (x,) for i in range(3)]

对于OP的具体例子:

from collections import namedtuple
import itertools as it

class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State, args in zip(it.repeat(State), [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ])]

我花了很多时间来理解为什么这是一个功能,而不是一个bug。

考虑下面的简单代码:

a = 5
def myfunc():
    print(a)

由于myfunc()中没有定义“a”,因此范围将扩大,代码将执行。

现在考虑类中的相同代码。它不能工作,因为这会完全扰乱对类实例中的数据的访问。你永远不会知道,你访问的是基类中的变量还是实例中的变量。

列表理解只是相同效果的一个子案例。


这可能是故意的,但恕我直言,这是一个糟糕的设计。我知道我不是这方面的专家,我已经试着阅读了这背后的基本原理,但我无法理解它,我认为任何普通的Python程序员都无法理解。

对我来说,理解似乎和正则数学表达式没有太大的不同。例如,如果'foo'是一个局部函数变量,我可以很容易地做这样的事情:

(foo + 5) + 7

但我做不到:

[foo + x for x in [1,2,3]]

对我来说,一个表达存在于当前的范围,而另一个表达创造了自己的范围,这是非常令人惊讶的,没有双关语的意思,“不可理解”。


可以使用for循环:

class A:
    x=5
##Won't work:
##    y=[i for i in range(101) if i%x==0]
    y=[]
    for i in range(101):
        if i%x==0:
            y.append(i)

请纠正我,我没有错…