小编典典

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

all

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


阅读 64

收藏
2022-06-13

共1个答案

小编典典

类范围和列表、集合或字典推导以及生成器表达式不混合。

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

在 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 中使用 set 或 dict 理解的类似示例会中断。

# 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您可以使用模块看到这一切。我在以下示例中使用
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()命名空间,包含xy用于创建类(除了它不起作用,因为x没有定义为全局)。注意,存入5x,会加载另一个代码对象;这就是列表理解;它被包裹在一个函数对象中,就像类体一样;创建的函数接受一个位置参数,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用来循环它并创建它的输出一样。

如果我们xfoo函数中定义,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]

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