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

2024-11-20 08:44:00
admin
原创
9
摘要:问题描述:如何从类定义中的列表推导访问其他类变量?以下方法在 Python 2 中有效,但在 Python 3 中失败:class Foo: x = 5 y = [x for i in range(1)] Python 3.11 给出错误:NameError: name 'x' is not ...

问题描述:

如何从类定义中的列表推导访问其他类变量?以下方法在 Python 2 中有效,但在 Python 3 中失败:

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

Python 3.11 给出错误:

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


解决方案 1:

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

原因;或者,官方的说法

您无法从包含在该范围内的函数、列表推导或生成器表达式访问类范围;它们的行为就像该范围不存在一样。在 Python 2 中,列表推导是使用快捷方式实现的,因此实际上可以访问类范围,但在 Python 3 中,它们有自己的范围(它们应该一直都有),因此您的示例会中断。无论 Python 版本如何,其他推导类型都有自己的范围,因此具有集合或字典推导的类似示例在 Python 2 中会中断。

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

更多详细信息

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

这在pep 227中有记录:

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

并在class复合语句文档中:

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

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

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

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

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

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

(小)例外;或者,为什么某个部件可能仍能工作

无论 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

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

查看引擎盖下的细节;或者比你想象的更加详细

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()包含x和的结果命名空间来创建类(但由于未定义为全局变量,y因此无法工作)。请注意,在存储后,它会加载另一个代码对象;这就是列表推导式;它像类主体一样被包装在函数对象中;创建的函数接受一个位置参数,即用于其循环代码的可迭代对象,并转换为迭代器。如字节码所示,在类范围内进行评估。x`5xrange(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 3.11)中拥有自己的代码对象,并且函数、生成器或推导式的代码对象之间没有区别;推导式代码对象被包装在临时函数对象中并立即调用。

  • 代码对象是在编译时创建的,任何非局部变量都会根据代码的嵌套范围标记为全局变量或自由变量。类主体不被视为查找这些变量的范围。

  • 执行代码时,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'),
       # ...
    ]]

PEP 709 是 Python 3.12 的一部分,它再次改变了其中的一些内容

在 Python 3.12 中,通过删除嵌套函数并内联循环,同时仍保持单独的范围,推导式变得更加高效。PEP 709 - 内联推导式中概述了如何实现此目的的详细信息,但简而言之,它不是创建新的函数对象,然后使用 和 字节码调用它LOAD_CONSTMAKE_FUNCTION而是CALL先将循环中使用的任何冲突名称移至堆栈,然后再内联执行推导式字节码。

需要注意的是,此更改仅影响性能,与类范围的交互没有变化。由于上述原因,您仍然无法访问在类范围中创建的名称。

使用 Python 3.12.0b4,该类的字节码Foo现在如下所示:

# creating `def foo()` and its bytecode elided

Disassembly of <code object Foo at 0x104e97000, file "<stdin>", line 2>:
  2           0 RESUME                   0
              2 LOAD_NAME                0 (__name__)
              4 STORE_NAME               1 (__module__)
              6 LOAD_CONST               0 ('foo.<locals>.Foo')
              8 STORE_NAME               2 (__qualname__)

  3          10 LOAD_CONST               1 (5)
             12 STORE_NAME               3 (x)

  4          14 PUSH_NULL
             16 LOAD_NAME                4 (range)
             18 LOAD_CONST               2 (1)
             20 CALL                     1
             28 GET_ITER
             30 LOAD_FAST_AND_CLEAR      0 (.0)
             32 LOAD_FAST_AND_CLEAR      1 (i)
             34 LOAD_FAST_AND_CLEAR      2 (x)
             36 SWAP                     4
             38 BUILD_LIST               0
             40 SWAP                     2
        >>   42 FOR_ITER                 8 (to 62)
             46 STORE_FAST               1 (i)
             48 LOAD_GLOBAL              6 (x)
             58 LIST_APPEND              2
             60 JUMP_BACKWARD           10 (to 42)
        >>   62 END_FOR
             64 SWAP                     4
             66 STORE_FAST               2 (x)
             68 STORE_FAST               1 (i)
             70 STORE_FAST               0 (.0)
             72 STORE_NAME               5 (y)
             74 RETURN_CONST             3 (None)

这里,最重要的字节码是偏移量 34 处的字节码:

             34 LOAD_FAST_AND_CLEAR      2 (x)

这将获取本地作用域中变量的值x并将其推送到堆栈上,然后清除x名称。如果当前作用域中没有变量,则会将 CNULL值存储在堆栈上。现在,名称已从本地作用域中消失,直到到达偏移量 66 处的字节码:

             66 STORE_FAST               2 (x)

这将恢复x到列表推导之前的状态;如果 aNULL存储在堆栈上以指示没有名为 的变量,那么执行此字节码后x仍然不会有变量。x

LOAD_FAST_AND_CLEAR和调用之间的其余字节码STORE_FAST与之前大致相同,SWAP使用字节码来访问对象的迭代器,range(1)而不是LOAD_FAST (.0)早期 Python 3.x 版本中的函数字节码。

解决方案 2:

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

新方法:

class Foo:
    x = 5
    y = (lambda x=x: [x for i in range(1)])()

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

解决方案 3:

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

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:

只需添加for x in [x]第一个for子句即可x在理解的范围内使用:

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

在你的另一种情况下for State in [State]

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State in [State] for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

或者有多个变量:

class Line:
    a = 19
    b = 4
    y = [a*x + b
         for a, b in [(a, b)]
         for x in range(10)]

解决方案 5:

由于最外层迭代器是在周围范围内评估的,因此我们可以zip一起使用itertools.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'],
        # ...
    ])]

解决方案 6:

可以使用for循环:

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

请纠正我,我没有错......

解决方案 7:

只是一个有趣的例子。

如果你想将其保留为列表推导式,这也适用于嵌套列表推导式。将值带出到全局命名空间,但保留其类名。

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

解决方案 8:

这是 Python 中的一个错误。人们宣称推导式等同于 for 循环,但在类中并非如此。至少在 Python 3.6.6 之前,在类中使用的推导式中,推导式内部只能访问推导式外部的一个变量,并且必须将其用作最外层的迭代器。在函数中,此范围限制不适用。

为了说明为什么这是一个错误,让我们回到原始示例。这失败了:

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

但这有效:

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

该限制在参考指南的本节末尾有所说明。

解决方案 9:

我花了很长时间才明白为什么这是一个功能,而不是一个错误。

考虑简单的代码:

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

由于 myfunc() 中没有定义“a”,因此范围将扩大并且代码将会执行。

现在考虑类中的相同代码。它无法工作,因为这会完全扰乱对类实例中数据的访问。你永远不会知道,你是在访问基类中的变量还是实例中的变量。

列表推导只是相同效果的一个子情况。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用