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

Mar*_*ato 152 python scope list-comprehension python-3.x python-internals

如何从类定义中的列表推导中访问其他类变量?以下适用于Python 2但在Python 3中失败:

class Foo:
    x = 5
    y = [x for i in range(1)]
Run Code Online (Sandbox Code Playgroud)

Python 3.2给出了错误:

NameError: global name 'x' is not defined
Run Code Online (Sandbox Code Playgroud)

尝试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'],
        # ...
    ]]
Run Code Online (Sandbox Code Playgroud)

在这个例子中,apply()本来是一个不错的解决方法,但它遗憾地从Python 3中删除.

Mar*_*ers 206

类范围和列表,集合或字典理解以及生成器表达式不会混合使用.

为什么; 或者,关于此的官方消息

在Python 3中,列表推导给出了它们自己的适当范围(本地命名空间),以防止它们的局部变量渗透到周围的范围内(参见Python列表理解重新绑定名称,即使在理解范围之后.这是正确的吗?).当在模块或函数中使用这样的列表推导时,这很好,但在类中,范围确定有点,嗯,奇怪.

这在pep 227中有记载:

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

并在class复合声明文件中:

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

强调我的; 执行帧是临时范围.

因为范围被重新用作类对象的属性,所以允许它用作非局部范围也会导致未定义的行为; 例如,如果一个类方法被称为x嵌套范围变量,那么会发生什么Foo.x呢?更重要的是,这对于子类意味着什么Foo?Python 必须以不同的方式处理类范围,因为它与函数范围非常不同.

最后,但绝对不是最不重要的,执行模型文档中的链接命名和绑定部分明确提到了类范围:

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

class A:
     a = 42
     b = list(a + i for i in range(10))
Run Code Online (Sandbox Code Playgroud)

因此,总结一下:您无法从该范围内的函数,列表推导或生成器表达式访问类范围; 他们的行为就像那个范围不存在一样.在Python 2中,列表推导是使用快捷方式实现的,但在Python 3中,它们有自己的功能范围(因为它们应该一直都有),因此您的示例会中断.无论Python版本如何,其他理解类型都有自己的范围,因此在Python 2中,类似于set或dict理解的示例会中断.

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

(小)例外; 或者,为什么一个部分仍然可以工作

无论Python版本如何,都在周围范围内执行理解或生成器表达式的一部分.这将是最外层迭代的表达式.在您的示例中,它是range(1):

y = [x for i in range(1)]
#               ^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

因此,x在该表达式中使用不会引发错误:

# Runs fine
y = [i for i in range(x)]
Run Code Online (Sandbox Code Playgroud)

这仅适用于最外面的可迭代; 如果一个理解有多个for子句,则内部for子句的迭代在评估范围内进行评估:

# NameError
y = [i for i in range(1) for j in range(x)]
Run Code Online (Sandbox Code Playgroud)

这个设计决策是为了在genexp创建时抛出错误,而不是迭代时创建生成器表达式的最外层迭代时抛出错误,或者当最外面的iterable结果不可迭代时.理解分享此行为是为了保持一致性.

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

您可以使用该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         
Run Code Online (Sandbox Code Playgroud)

第一个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         
Run Code Online (Sandbox Code Playgroud)

上面的字节码创建了类体.执行该函数以及生成的locals()命名空间,包含xy用于创建类(除非它不起作用,因为x它未定义为全局).请注意,在存储5之后x,它会加载另一个代码对象; 这就是列表理解; 它被包装在一个函数对象中,就像类体一样; created函数接受一个位置参数,range(1)用于循环代码的iterable ,强制转换为迭代器.如字节码所示,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        
Run Code Online (Sandbox Code Playgroud)

没有加载代码对象,而是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         
Run Code Online (Sandbox Code Playgroud)

这块字节码加载传入的第一个参数(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         
Run Code Online (Sandbox Code Playgroud)

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]
Run Code Online (Sandbox Code Playgroud)

实际引用从当前帧数据结构中查找值,这些结构是从函数对象的.__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
Run Code Online (Sandbox Code Playgroud)

所以,总结一下:

  • 列表推导在Python 3中获得自己的代码对象,并且函数,生成器或理解的代码对象之间没有区别; 理解代码对象包装在临时函数对象中并立即调用.
  • 代码对象是在编译时创建的,并且任何非局部变量都基于代码的嵌套范围标记为全局变量或自由变量.类主体被视为查找这些变量的范围.
  • 在执行代码时,Python只需查看全局变量或当前正在执行的对象的闭包.由于编译器未将类主体包含在作用域中,因此不考虑临时函数名称空间.

解决方法; 或者,该怎么做

如果要为x变量创建显式范围,就像在函数中一样,可以使用类范围变量来实现列表推导:

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

'临时' 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',)
Run Code Online (Sandbox Code Playgroud)

当然,阅读你的代码的人会在这一点上划伤; 你可能想在那里写一个很大的评论来解释你为什么要这样做.

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

def __init__(self):
    self.y = [self.x for i in range(1)]
Run Code Online (Sandbox Code Playgroud)

并避免所有头疼,并解释自己的问题.对于你自己的具体例子,我甚至不会把它存放namedtuple在课堂上; 要么直接使用输出(根本不存储生成的类),要么使用全局:

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

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

  • 你也可以使用lambda来修复绑定:`y =(lambda x = x:[x for i in range(1)])()` (21认同)
  • 如果它需要一页技术信息来解释为什么某些东西不能直观地工作,我称之为一个错误. (7认同)
  • @ecatmur:毕竟,`lambda`只是匿名函数. (3认同)
  • @JonathanLeaders:不要将它称为_bug_,将其称为_tradeoff_.如果你想要A和B,但只能得到其中一个,那么不管你怎么决定,在某些情况下你会不喜欢结果.这就是生活. (3认同)
  • 对于记录,使用默认参数(对于lambda或函数)传递类变量的变通方法有一个问题.即,它传递变量的_current value_.因此,如果变量稍后更改,然后调用lambda或函数,则lambda或函数将使用旧值.此行为不同于闭包的行为(它将捕获对变量的引用,而不是其值),因此可能是意外的. (2认同)

Jon*_*han 13

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

Old Way(适用于2.7,投掷NameError: name 'x' is not defined3+):

class A:
    x = 4
    y = [x+i for i in range(1)]
Run Code Online (Sandbox Code Playgroud)

注意:简单地使用它来确定它并A.x不能解决问题

新方式(适用于3+):

class A:
    x = 4
    y = (lambda x=x: [x+i for i in range(1)])()
Run Code Online (Sandbox Code Playgroud)

  • 当使用生成器表达式以及集合和字典理解时,问题也存在于Python 2中.它不是一个错误,它是类命名空间如何工作的结果.它不会改变. (6认同)
  • 我注意到你的解决方法完全符合我的回答:创建一个新的范围(lambda与使用`def`创建函数没有什么不同). (4认同)

FMc*_*FMc 5

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

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)
Run Code Online (Sandbox Code Playgroud)