代码之家  ›  专栏  ›  技术社区  ›  Rob Kwasowski george

python:类内的字典理解[复制]

  •  1
  • Rob Kwasowski george  · 技术社区  · 6 年前

    如何从类定义中的列表理解访问其他类变量?以下操作在Python2中有效,但在Python3中失败:

    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中删除了。

    0 回复  |  直到 6 年前
        1
  •  190
  •   Martijn Pieters    6 年前

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

    原因;或者,官方的说法

    在Python3中,列表理解被赋予了一个适当的作用域(本地名称空间),以防止它们的本地变量溢出到周围的作用域中(请参见 Python list comprehension rebind names even after scope of comprehension. Is this right? )。在模块或函数中使用这样的列表理解非常好,但是在类中,范围界定有点,嗯, 奇怪的 .

    这记录在 pep 227 :

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

    而在 class compound statement documentation :

    然后在新的执行框架中执行classs套件(参见 Naming and binding ,使用新创建的本地命名空间和原始全局命名空间。(通常,该套件只包含函数定义。)当类的套件完成执行时, 其执行帧被丢弃,但其本地命名空间被保存 . [4] 然后使用基类的继承列表和属性字典保存的本地命名空间创建类对象。

    重点挖掘;执行框架是临时范围。

    因为作用域被重新用作类对象的属性,允许它用作非本地作用域也会导致未定义的行为;如果类方法引用 x 作为嵌套范围变量,然后操纵 Foo.x 还有,比如说?更重要的是,这对 Foo ?蟒蛇 以不同的方式对待类作用域,因为它与函数作用域非常不同。

    最后,但绝对不是最不重要的是 Naming and binding 执行模型文档中的部分明确提到了类作用域:

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

    class A:
         a = 42
         b = list(a + i for i in range(10))
    

    因此,总结一下:不能从包含在该作用域中的函数、列表理解或生成器表达式访问类作用域;它们的作用就像该作用域不存在一样。在Python2中,列表理解是使用快捷方式实现的,但在Python3中,它们有自己的函数作用域(它们应该一直都有),因此示例中断。不管python版本如何,其他理解类型都有自己的作用域,因此在python 2中会出现一个带有set或dict理解的类似示例。

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

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

    理解或生成器表达式的一部分在周围的范围内执行,而不管python版本如何。这将是最外层iterable的表达式。在你的例子中,是 range(1) :

    y = [x for i in range(1)]
    #               ^^^^^^^^
    

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

    # Runs fine
    y = [i for i in range(x)]
    

    这只适用于最外层的iterable;如果一个理解有多个 for 子句,内部的iterables 对于 从句在理解范围内进行评估:

    # NameError
    y = [i for i in range(1) for j in range(x)]
    

    这个设计决策是为了在genexp创建时抛出错误,而不是在创建生成器表达式的最外层iterable时抛出错误,或者在最外层iterable结果不是iterable时抛出错误。理解共享这种行为以保持一致性。

    从引擎盖下面看;或者,比你想要的要详细得多

    您可以使用 dis module . 我在下面的示例中使用Python3.3,因为它添加了 qualified names 能够清楚地识别我们要检查的代码对象。生成的字节码在功能上与Python3.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 这里为 类body,然后将其转换为函数并调用它。这个 结果 然后使用该调用创建类的命名空间 __dict__ . 到现在为止,一直都还不错。

    这里要注意的是,字节码包含一个嵌套的代码对象;在python中,类定义、函数、理解和生成器都表示为代码对象,这些对象不仅包含字节码,还包含表示局部变量、常量、从g中获取的变量的结构。从嵌套作用域获取的lobals和变量。编译的字节码引用那些结构,而python解释器知道如何访问给定字节码的那些结构。

    这里要记住的重要一点是,python在编译时创建这些结构; suite是一个代码对象( <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 ,它加载另一个代码对象;这是列表理解;它像类主体一样被包装在函数对象中;创建的函数接受一个位置参数, 范围(1) 可用于循环代码,转换为迭代器。如字节码所示, 范围(1) 在类作用域中求值。

    从中可以看出,函数或生成器的代码对象与理解的代码对象之间的唯一区别是后者是执行的 立即 当父代码对象被执行时;字节码只是动态地创建一个函数,然后用几个小步骤执行它。

    python 2.x在那里使用内联字节码,这里是python2.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 循环以内联方式运行。所以在Python3.x中,列表生成器被赋予了一个自己的适当代码对象,这意味着它有自己的作用域。

    然而,当解释器第一次加载模块或脚本时,理解是与python源代码的其余部分一起编译的,而编译器确实 将类套件视为有效范围。列表理解中的任何引用变量都必须在作用域中查找 周围的 类定义,递归地。如果编译器找不到该变量,则将其标记为全局变量。对list comprehension code对象的反汇编表明 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         
    

    这个字节码块加载传入的第一个参数 范围(1) 迭代器),就像python 2.x版本使用 福里斯特 循环它并创建它的输出。

    我们确定了吗 X foo 改为函数, 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__ 属性。由于为comprehension code对象创建的函数再次被丢弃,我们无法检查该函数的闭包。要查看闭包的运行情况,我们必须检查嵌套函数:

    >>> 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只需查看globals或当前执行对象的闭包。由于编译器没有将类体作为作用域包含,因此不考虑临时函数命名空间。

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

    如果要为 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'),
           # ...
        ]]
    
        2
  •  12
  •   Jonathan    6 年前

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

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

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

    因为语法太难看了,所以我只需在构造函数中初始化所有类变量

        3
  •  5
  •   FMc TLP    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)
    
        4
  •  1
  •   bzip2    6 年前

    这是python中的一个bug。理解被宣传为等同于for循环,但这在类中是不正确的。至少在Python3.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)]
    

    限制在 this section 在参考指南中。

        5
  •  0
  •   a_guest    6 年前

    因为最外层的迭代器是在周围的作用域中计算的,所以我们可以使用 zip 一起 itertools.repeat 将依赖关系转移到理解的范围:

    import itertools as it
    
    class Foo:
        x = 5
        y = [j for i, j in zip(range(3), it.repeat(x))]
    

    也可以使用嵌套 for 在理解中循环,并在最外层iterable中包含依赖项:

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