python-3.x 从类定义中的列表解析访问类变量

5w9g7ksd  于 2023-04-13  发布在  Python
关注(0)|答案(9)|浏览(159)

如何从类定义中的列表解析访问其他类变量?下面的方法在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中删除了。

ztyzrc3y

ztyzrc3y1#

只是一个有趣的例子。
如果你想把它保留为列表解析,这也适用于嵌套列表解析。把值带出全局名称空间,但保留它的类名。

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

问候

oyxsuwqo

oyxsuwqo2#

我花了相当长的时间来理解为什么这是一个功能,而不是一个错误。
考虑简单的代码:

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

由于myfunc()中没有定义“a”,因此范围将扩展,代码将执行。
现在考虑类中的相同代码。它不能工作,因为这将完全扰乱访问类示例中的数据。你永远不会知道,你是在访问基类还是示例中的变量。
列表解析只是相同效果的一个子案例。

sg2wtvxw

sg2wtvxw3#

  • 类作用域和列表、集合或字典解析,以及生成器表达式不能混用。*

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

在Python 3中,列表解析被赋予了自己的适当作用域(本地命名空间),以防止其局部变量溢出到周围的作用域(参见List comprehension rebinds names even after scope of comprehension. Is this right?)。当在模块或函数中使用这样的列表解析时,这很好,但在类中,作用域有点,嗯,* 奇怪 *。
这记录在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)]

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

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

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

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

你可以使用dis module看到这一切。在下面的例子中,我使用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类主体的代码对象,然后将其变成一个函数并调用它。然后调用的 result 用于创建类的命名空间,即__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没有定义为全局)。注意,在将5存储到x中之后,它加载了另一个代码对象;这就是列表理解;它被 Package 在一个函数对象中,就像类体一样;创建的函数接受一个位置参数,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

没有加载代码对象,而是内联运行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函数中定义xx将是一个单元格变量(单元格引用嵌套的作用域):

>>> 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将从代码对象cell对象间接加载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中获得自己的代码对象,函数、生成器或解析的代码对象之间没有区别;理解代码对象被 Package 在临时函数对象中并被立即调用。
  • 代码对象是在编译时创建的,任何非局部变量都被标记为全局变量或自由变量,这取决于代码的嵌套范围。类主体 * 不 * 被认为是查找这些变量的范围。
  • 当执行代码时,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'),
       # ...
    ]]
rkttyhzu

rkttyhzu4#

在我看来,这是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)])()

因为语法是如此丑陋,我只是初始化所有我的类变量在构造函数通常

oknrviil

oknrviil5#

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

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

ddhy6vgd6#

由于最外层的迭代器是在周围的作用域中计算的,我们可以使用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)]

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

jvlzgdj97#

我知道我不是这方面的Maven,我也试过阅读这背后的原理,但它只是超出了我的理解范围,就像我认为任何普通的Python程序员一样。
对我来说,一个理解和一个正则数学表达式似乎没有太大的区别。例如,如果'foo'是一个局部函数变量,我可以很容易地这样做:

(foo + 5) + 7

但我做不到:

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

对我来说,一个表达式存在于当前作用域中,而另一个表达式创建了自己的作用域,这是非常令人惊讶的,而且,没有双关语,“无法理解”。

irlmq6kh

irlmq6kh8#

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

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

zvokhttg

zvokhttg9#

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

请纠正我我没有错。。

相关问题