在类定义中从列表推导式中访问类变量

245

如何在类定义内的列表推导式中访问其他类变量?以下代码可以在Python 2中运行但在Python 3中会出错:

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

Python 3.2 报错:

NameError: global name 'x' is not defined

尝试使用Foo.x也不起作用。有没有关于如何在Python 3中执行此操作的想法?

一个略微更复杂的示例:

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

在这个例子中,apply()本来可以作为一个不错的解决方法,但遗憾的是它在Python 3中被移除了。


有趣...一个明显的解决方法是在类定义退出后分配y。Foo.y = [Foo.x for i in range(1)] - gps
3
+martijn-pieters 提供的重复链接是正确的,其中有一条 +matt-b 的评论解释了原因:Python 2.7 中的列表推导式没有自己的命名空间(不像集合、字典推导式或生成器表达式...将 [] 替换为 {} 可以看到这一点)。在Python3中,所有 列表推导式都有自己的命名空间。 - gps
@gps:或者使用嵌套作用域,在类定义套件中插入一个(临时)函数。 - Martijn Pieters
我刚在2.7.11上进行了测试。出现了名称错误。 - Junchao Gu
10个回答

354
类的范围和列表、集合或字典推导式以及生成器表达式不能混合使用。
为什么会这样呢?或者说,官方对此的解释是什么?
在Python 3中,列表推导式被赋予了自己的适当作用域(局部命名空间),以防止它们的局部变量泄漏到周围的作用域中(参见列表推导式在作用域之后重新绑定名称。这样做对吗?)。当在模块或函数中使用这样的列表推导式时,这非常好,但在类中,作用域有点,嗯,奇怪。
这在pep 227中有记录:
类作用域中的名称不可访问。名称在最内层的封闭函数作用域中解析。如果类定义出现在一系列嵌套作用域中,则解析过程会跳过类定义。
还有在class复合语句文档中也有相关说明:
类的套件然后在一个新的执行框架中执行(参见命名和绑定),使用新创建的本地命名空间和原始全局命名空间。(通常,套件只包含函数定义。)当类的套件执行完成时,它的执行框架被丢弃,但其本地命名空间被保存[4] 然后使用继承列表为基类创建一个类对象,并使用保存的本地命名空间作为属性字典。
重点是,执行框架是临时作用域。
因为作用域被重新用作类对象上的属性,允许它作为非局部作用域使用会导致未定义的行为;例如,如果一个类方法引用了x作为嵌套作用域变量,然后还操作了Foo.x,那会发生什么?更重要的是,这对于Foo的子类意味着什么?Python必须将类作用域与函数作用域区别对待,因为它们之间非常不同。
最后但绝对不是最不重要的,执行模型文档中链接的命名和绑定部分明确提到了类作用域。

The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope. This means that the following will fail:

class A:
     a = 42
     b = list(a + i for i in range(10))
所以,总结一下:你无法从函数、列表推导式或生成器表达式中访问类作用域;它们的行为就像该作用域不存在一样。在Python 2中,列表推导式使用了一个快捷方式,但在Python 3中,它们有了自己的函数作用域(正如它们一直应该有的),因此你的示例会出错。其他推导类型不管Python版本如何,都有自己的作用域,所以在Python 2中,使用集合推导式或字典推导式的类似示例也会出错。
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}

(小)例外情况;或者,为什么一个部分可能仍然有效

无论Python版本如何,理解或生成器表达式中的一部分都会在周围的作用域中执行。这将是最外层可迭代对象的表达式。在您的示例中,它是range(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

这个设计决策是为了在生成器表达式的最外层可迭代对象抛出错误时,在生成器表达式创建时而不是迭代时抛出错误,或者当最外层可迭代对象实际上不可迭代时抛出错误。为了保持一致性,推导式也共享这种行为。
深入了解;或者说,比你想要的更详细
你可以使用dis模块来观察这一切的运作。我在下面的示例中使用的是Python 3.3,因为它添加了可以清晰地标识我们想要检查的代码对象的限定名称。生成的字节码在功能上与Python 3.2完全相同。
创建一个类,Python基本上会将构成类体的整个套件(即比class <name>:行缩进一级的所有内容)作为一个函数执行:
>>> 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在编译时创建这些结构;class套件是一个已经编译的代码对象(<code object Foo at 0x10a436030, file "<stdin>", line 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         

上述的字节码创建了类体。函数被执行,产生的locals()命名空间包含x和y,用于创建类(但由于x没有定义为全局变量,所以无法工作)。请注意,在将5存储在x中之后,它加载另一个代码对象;那就是列表推导式;它被包装在一个函数对象中,就像类体一样;创建的函数接受一个位置参数,即用于循环代码的range(1)可迭代对象,转换为迭代器。如字节码所示,range(1)在类作用域中进行评估。
从这里可以看出,函数或生成器的代码对象与推导式的代码对象之间唯一的区别是,当父代码对象执行时,后者会立即执行;字节码只是在几个小步骤中动态创建并执行函数。
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循环。因此,在Python 3.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

所以,总结一下:

  • 在Python 3(直到Python 3.11)中,列表推导式有自己的代码对象,函数、生成器和推导式之间没有区别;推导式代码对象被包装在临时函数对象中并立即调用。
  • 代码对象在编译时创建,并根据代码的嵌套作用域将任何非局部变量标记为全局变量或自由变量。类体不被视为查找这些变量的作用域。
  • 在执行代码时,Python 只需要查看全局变量或当前执行对象的闭包。由于编译器没有将类体包括在作用域中,因此临时函数命名空间不会被考虑。

解决方法;或者如何处理它

如果您为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'),
       # ...
    ]]

PEP 709,作为Python 3.12的一部分,再次改变了一些内容。
在Python 3.12中,通过移除嵌套函数并内联循环,大大提高了推导式的效率,同时仍保持独立的作用域。具体实现细节可以参考PEP 709 - Inlined comprehensions,但简单来说,不再创建新的函数对象并调用它,而是通过LOAD_CONSTMAKE_FUNCTIONCALL字节码,将循环中使用的冲突名称先移到堆栈上,然后内联执行推导式字节码。
需要注意的是,这个改变只影响性能,与类作用域的交互没有改变。你仍然无法访问在类作用域中创建的名称,原因如上所述。
使用Python 3.12.0b4,Foo类的字节码现在如下所示:
# creating `def foo()` and its bytecode elided

Disassembly of <code object Foo at 0x104e97000, file "<stdin>", line 2>:
  2           0 RESUME                   0
              2 LOAD_NAME                0 (__name__)
              4 STORE_NAME               1 (__module__)
              6 LOAD_CONST               0 ('foo.<locals>.Foo')
              8 STORE_NAME               2 (__qualname__)

  3          10 LOAD_CONST               1 (5)
             12 STORE_NAME               3 (x)

  4          14 PUSH_NULL
             16 LOAD_NAME                4 (range)
             18 LOAD_CONST               2 (1)
             20 CALL                     1
             28 GET_ITER
             30 LOAD_FAST_AND_CLEAR      0 (.0)
             32 LOAD_FAST_AND_CLEAR      1 (i)
             34 LOAD_FAST_AND_CLEAR      2 (x)
             36 SWAP                     4
             38 BUILD_LIST               0
             40 SWAP                     2
        >>   42 FOR_ITER                 8 (to 62)
             46 STORE_FAST               1 (i)
             48 LOAD_GLOBAL              6 (x)
             58 LIST_APPEND              2
             60 JUMP_BACKWARD           10 (to 42)
        >>   62 END_FOR
             64 SWAP                     4
             66 STORE_FAST               2 (x)
             68 STORE_FAST               1 (i)
             70 STORE_FAST               0 (.0)
             72 STORE_NAME               5 (y)
             74 RETURN_CONST             3 (None)

这里,最重要的字节码是偏移量为34的那个。
             34 LOAD_FAST_AND_CLEAR      2 (x)

这将在局部作用域中获取变量x的值并将其推送到堆栈上,然后清除该名称。如果当前作用域中没有变量x,则在堆栈上存储一个C的NULL值。现在,该名称已经从局部作用域中消失,直到达到偏移量为66的字节码位置。
             66 STORE_FAST               2 (x)

这将恢复x到列表推导之前的状态;如果在堆栈上存储了一个NULL来表示没有名为x的变量,那么在执行完这段字节码后仍然不会有变量x
LOAD_FAST_AND_CLEARSTORE_FAST调用之间的其余字节码与之前几乎相同,只是使用SWAP字节码来访问range(1)对象的迭代器,而不是在早期Python 3.x版本中的函数字节码中使用LOAD_FAST (.0)

33
你也可以使用 lambda 表达式来修复绑定:y = (lambda x=x: [x for i in range(1)])() - ecatmur
6
没错,lambda本质上就是匿名函数。 - Martijn Pieters
6
记录一下,使用默认参数(传递给 lambda 或函数)的解决方法有一个陷阱。即它传递的是变量的当前值。因此,如果变量稍后更改,然后调用 lambda 或函数,则 lambda 或函数将使用旧值。这种行为与闭包的行为不同(闭包会捕获对变量的引用而不是其值),因此可能出乎意料。 - Neal Young
17
如果需要一页技术信息来解释为什么某个东西不直观地工作,我称之为缺陷。 - Jonathan
8
@JonathanLeaders:不要称其为“漏洞”,而应称其为“权衡”。如果你想要A和B,但只能得到其中之一,那么无论如何决定,在某些情况下你都会不喜欢结果。这就是生活。 - Lutz Prechelt
显示剩余3条评论

25

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

旧的方式(在2.7中有效,在3+中会抛出NameError: name 'x' is not defined):

class A:
    x = 4
    y = [x+i for i in range(1)]
< p >< em >注意:仅使用< code >A.x< /code >进行作用域限定并不能解决此问题。

新方法(适用于3+):

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

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


7
这个问题在使用生成器表达式、集合和字典推导时,Python 2 中也存在。这不是一个错误,而是类命名空间工作方式的结果。它不会改变。 - Martijn Pieters
4
我注意到你的解决方法与我的答案已经说明的一样:创建一个新的作用域(在这里,使用lambda创建函数与使用def创建函数没有任何区别)。 - Martijn Pieters
1
是的。虽然一眼看到解决方法很好,但这个答案错误地将行为描述为一个错误,而实际上它是语言工作方式的副作用(因此不会更改)。 - jsbueno
这是一个不同的问题,在Python 3中实际上并不是问题。它只会在使用python -c“import IPython; IPython.embed()”以嵌入模式调用IPython时出现。直接使用ipython运行IPython,问题将消失。 - Rian Rizvi

7

接受的答案提供了很好的信息,但这里似乎还有一些其他细节——列表推导和生成器表达式之间的差异。我尝试了一个演示:

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 2 和 3 之间的实现方式有所改变,请查看 Python 2 中,列表推导式使用了一种快捷方式来实现,但在 Python 3 中,它们拥有了自己的函数作用域(正如它们一直应该有的那样),因此你的示例会出错 - Martijn Pieters
我的答案也涵盖了你的解决方法:通过创建一个 lambdadef,你可以创建一个新的作用域,正如“解决方法”部分所述:如果你为变量 x 创建一个显式作用域,比如在一个函数中,你就可以在列表推导式中使用类作用域变量 - Martijn Pieters
Z5示例遵循“异常”部分:无论Python版本如何,理解式或生成器表达式的一部分都会在周围范围内执行。那就是最外层可迭代对象的表达式。在这里,它是range(len(XS));该表达式的结果作为可迭代对象传递到生成器表达式范围中。这也是为什么您不能在生成器表达式中的任何其他地方引用XS;它不是传入的名称,而是传入名称引用的对象,它是理解范围内的局部变量。 - Martijn Pieters
@MartijnPieters 我非常确定在2018年8月5日的情况看起来是不同的。 - FMc
经过查看编辑历史和回忆起来,您在2018年8月之后添加的内容是我当时在尝试这个主题时感到困惑的一部分。这个难题促使我编写了这段代码并发布了答案。因此,在2018年8月,情况确实有所不同。 - FMc
显示剩余2条评论

2
可以使用一个 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)

请纠正我,如果我错了...


1
是的,这个方法有效。虽然有些笨拙,但没有什么不好的地方:它和其他选择一样好(或者不好)。 - WestCoastProjects

2

由于最外层迭代器在周围作用域中进行评估,因此我们可以使用zipitertools.repeat将依赖项传递到理解的范围内:


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)]

针对原作者的具体问题:

最初的回答:

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'],
        # ...
    ])]

2
只需将for x in [x]作为第一个for子句添加,以使x在推导式的范围内可用:
class Foo:
    x = 5
    y = [x for x in [x] for i in range(1)]

而且在你的其他情况中,使用for State in [State]
from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State in [State] for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

或者有多个变量:
class Line:
    a = 19
    b = 4
    y = [a*x + b
         for a, b in [(a, b)]
         for x in range(10)]

太丑了,让人无法接受,而且在嵌套的函数范围内它仍然不起作用。 - ingyhere
@ingyhere 嵌套函数作用域是什么?请举个例子。 - Kelly Bundy
请阅读顶部评论。你不会解决这个问题——这是语言的缺陷。看看这里的挥手……创建一个集合来传递一个简单的标量。尝试一些更复杂的数据类型,或者在re.sub()操作中使用预编译的正则表达式模式和复杂的dict()值。尽管推导的目的是优雅、紧凑和方便,但添加多余的数据集合和额外的子句并不能充分展现这种语言结构的价值。谢谢,但我已经对这个兔子洞无能为力了。 - ingyhere

1

这可能是故意设计的,但在我看来,这是一个糟糕的设计。我知道我在这方面不是专家,我尝试过阅读背后的原理,但它对我来说太深奥了,我认为对于任何普通的Python程序员也是如此。

对我来说,列表推导式似乎并没有太大的区别,就像一个普通的数学表达式。例如,如果“foo”是一个本地函数变量,我可以轻松地做一些类似这样的事情:

(foo + 5) + 7

但我做不到:

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

在当前的范围内存在一个表达式,而另一个则创建了自己的范围,这一事实对我来说非常令人惊讶和难以理解,无意冒犯。

这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Vuks

0

一个有趣的例子。

如果你想把它保留为列表推导式,这也可以使用嵌套列表推导式。 将值带到全局命名空间中,但保留其类名。

class Foo:
  global __x
  __x = 5
  y = [_Foo__x for i in range(1)]

敬礼


-1

这是 Python 中的一个 bug。推广时,理解生成式与 for 循环等价,但在类中并非如此。至少到 Python 3.6.6 为止,在类中使用生成式时,只有一个来自生成式外部的变量可以在生成式内部访问,并且它必须用作最外层的迭代器。在函数中,不存在这种作用域限制。

为了说明这是一个 bug,让我们回到原始示例。这将失败:

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

但这个可以工作:
def Foo():
    x = 5
    y = [x for i in range(1)]

该限制在参考指南中本节的结尾处说明。


2
这不是一个错误。这是按设计,这就是类作用域的工作方式,也是为什么名称解析参考文档明确指出它会失败的原因。 - Martijn Pieters

-2

我花了相当长的时间才明白这是一个特性而不是一个错误。

考虑下面这段简单的代码:

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

由于在myfunc()中没有定义"a",作用域会扩大并且代码将执行。

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

列表推导式只是相同效果的一个子情况。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接