从类定义中的列表理解访问类变量

新手上路,请多包涵

您如何从类定义中的列表理解访问其他类变量?以下在 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 中删除。

原文由 Mark Lodato 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 543
2 个回答

类作用域和列表、集合或字典理解以及生成器表达式不会混合。

为什么;或者,关于这个的官方说法

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

这记录在 pep 227 中:

类范围内的名称不可访问。名称在最里面的封闭函数范围内解析。如果类定义出现在嵌套作用域链中,解析过程将跳过类定义。

class 复合语句文档中

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

强调我的;执行框架是临时范围。

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

最后但同样重要的是,执行模型文档中链接的 命名和绑定 部分明确提到了类作用域:

类块中定义的名称范围仅限于类块;它不会扩展到方法的代码块——这包括理解和生成器表达式,因为它们是使用函数作用域实现的。这意味着以下将失败:

>  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


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

## 看看引擎盖下;或者,比你想要的更详细

您可以使用 [`dis` 模块](http://docs.python.org/3/library/dis.html) 查看所有操作。我在以下示例中使用 Python 3.3,因为它添加了 [限定名称](http://docs.python.org/3/whatsnew/3.3.html#pep-3155-qualified-name-for-classes-and-functions),可以巧妙地标识我们要检查的代码对象。生成的字节码在其他方面与 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 (”, 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, ”, 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..Foo’) 13 STORE_NAME 2 (qualname)

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

4 22 LOAD_CONST 2 ( at 0x10a385420, file “”, line 4>) 25 LOAD_CONST 3 (‘foo..Foo.’) 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..Foo’, 5, at 0x10a385420, file “”, line 4>, ‘foo..Foo.’, 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 只需查看全局变量或当前正在执行的对象的闭包。由于编译器没有将类主体作为作用域,因此不考虑临时函数命名空间。

## 解决方法;或者,该怎么办

如果你要为 `x` 变量创建一个显式范围,就像在函数中一样,你 _可以_ 使用类范围变量来理解列表:

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


'temporary' `y` 函数可以直接调用;我们在处理它的返回值时替换它。解析 `x` _时_ 考虑其范围:

foo.code.co_consts[1].co_consts[2] ”, 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’), # … ]]

”`

原文由 Martijn Pieters 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

旧方法(在 2.7 中工作,在 3+ 中抛出 NameError: name 'x' is not defined ):

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

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

原文由 Jonathan 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题