理解的符号表中的这些额外符号是什么?

What are these extra symbols in a comprehension's symtable?

我正在使用 symtable 获取一段代码的符号表。奇怪的是,在使用理解(listcomp、setcomp 等)时,有一些额外的符号我没有定义。

复制(使用 CPython 3.6):

import symtable

root = symtable.symtable('[x for x in y]', '?', 'exec')
# Print symtable of the listcomp
print(root.get_children()[0].get_symbols())

输出:

[<symbol '.0'>, <symbol '_[1]'>, <symbol 'x'>]
需要

符号 x。但是 .0_[1] 是什么?

请注意,对于任何其他非理解结构,我得到的标识符与我在代码中使用的标识符完全相同。例如,lambda x: y 只会产生符号 [<symbol 'x'>, <symbol 'y'>].

另外,文档说 symtable.Symbol 是...

An entry in a SymbolTable corresponding to an identifier in the source.

...尽管这些标识符显然没有出现在源代码中。

因此,列表推导的实现方式实际上是通过创建一个代码对象,这有点像创建一个一次性使用的匿名函数,用于范围界定:

>>> import dis
>>> def f(y): [x for x in y]
...
>>> dis.dis(f)
  1           0 LOAD_CONST               1 (<code object <listcomp> at 0x101df9db0, file "<stdin>", line 1>)
              3 LOAD_CONST               2 ('f.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_FAST                0 (y)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 POP_TOP
             17 LOAD_CONST               0 (None)
             20 RETURN_VALUE
>>>

检查代码对象,我可以找到 .0 符号:

>>> dis.dis(f.__code__.co_consts[1])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                12 (to 21)
              9 STORE_FAST               1 (x)
             12 LOAD_FAST                1 (x)
             15 LIST_APPEND              2
             18 JUMP_ABSOLUTE            6
        >>   21 RETURN_VALUE

请注意,list-comp 代码对象中的 LOAD_FAST 似乎正在加载未命名的参数,它对应于 GET_ITER

这两个名字是用来实现列表推导式的,作为一个单独的作用域,它们的含义如下:

  • .0 是一个 隐式参数 ,用于可迭代对象(在您的案例中来自 y)。
  • _[1] 是符号 table 中的临时名称,用于 目标列表 。该列表最终会出现在堆栈中。*

列表理解(以及字典和集合理解以及生成器表达式)在新范围内执行。为此,Python 有效地创建了一个新的匿名函数。

因为它是一个函数,真的,你需要传入你正在循环的可迭代对象作为参数。这就是 .0 的用途,它是第一个隐式参数(因此在索引 0 处)。您生成的符号 table 明确列出 .0 作为参数:

>>> root = symtable.symtable('[x for x in y]', '?', 'exec')
>>> type(root.get_children()[0])
<class 'symtable.Function'>
>>> root.get_children()[0].get_parameters()
('.0',)

您的 table 的第一个子函数是一个带有一个名为 .0 的参数的函数。

列表理解还需要构建输出列表,并且该列表也可以被视为本地列表。这是 _[1] 临时变量。它实际上从未成为生成的代码对象中的命名局部变量;这个临时变量保存在堆栈中。

可以看到使用compile():

时产生的代码对象
>>> code_object = compile('[x for x in y]', '?', 'exec')
>>> code_object
<code object <module> at 0x11a4f3ed0, file "?", line 1>
>>> code_object.co_consts[0]
<code object <listcomp> at 0x11a4ea8a0, file "?", line 1>

所以有一个外部代码对象,在常量中,是另一个嵌套代码对象。后一个是循环的实际代码对象。它使用 .0x 作为局部变量。它还需要 1 个参数;参数的名称是 co_varnames 元组中的第一个 co_argcount 值:

>>> code_object.co_consts[0].co_varnames
('.0', 'x')
>>> code_object.co_consts[0].co_argcount
1

所以 .0 是这里的参数名称。

_[1]临时变量是在栈上处理的,看反汇编:

>>> import dis
>>> dis.dis(code_object.co_consts[0])
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE

这里我们再次看到 .0 被引用。 _[1]BUILD_LIST 操作码将列表对象推入堆栈,然后 .0 被放入堆栈以供 FOR_ITER 操作码迭代(操作码从中删除可迭代对象) .0 再次从堆栈中取出)。

每个迭代结果被FOR_ITER压入栈中,用STORE_FAST再次弹出并存储在x中,然后用LOAD_FAST再次加载入栈。最后 LIST_APPEND 从堆栈中取出顶部元素,并将其添加到堆栈中下一个元素引用的列表中,因此 _[1].

JUMP_ABSOLUTE 然后将我们带回到循环的顶部,在那里我们继续迭代直到迭代完成。最后,RETURN_VALUEreturns栈顶,再次_[1],给调用者。

外部代码对象完成加载嵌套代码对象并将其作为函数调用的工作:

>>> dis.dis(code_object)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x11a4ea8a0, file "?", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (y)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

所以这创建了一个函数对象,函数名为 <listcomp>(有助于回溯),加载 y,为它生成一个迭代器(相当于 iter(y),并以该迭代器作为参数调用函数。

如果你想把它翻译成伪代码,它看起来像:

def <listcomp>(.0):
    _[1] = []
    for x in .0:
        _[1].append(x)
    return _[1]

<listcomp>(iter(y))

生成器表达式当然不需要_[1]临时变量:

>>> symtable.symtable('(x for x in y)', '?', 'exec').get_children()[0].get_symbols()
[<symbol '.0'>, <symbol 'x'>]

生成器表达式函数对象生成值,而不是附加到列表:

>>> dis.dis(compile('(x for x in y)', '?', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    2 FOR_ITER                10 (to 14)
              4 STORE_FAST               1 (x)
              6 LOAD_FAST                1 (x)
              8 YIELD_VALUE
             10 POP_TOP
             12 JUMP_ABSOLUTE            2
        >>   14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

连同外层字节码,生成器表达式等价于:

def <genexpr>(.0):
    for x in .0:
        yield x

<genexpr>(iter(y))

*临时变量其实已经不需要了;它们用于推导式的初始实现,但 this commit from April 2007 moved the compiler to just using the stack, and this has been the norm for all of the 3.x releases, as well as Python 2.7. It still is easier to think of the generated name as a reference to the stack. Because the variable is no longer needed, I filed issue 32836 将其删除,并且 Python 3.8 及更高版本将不再将其包含在符号 table.

在Python2.6中,反汇编中仍能看到实际的临时名称:

>>> import dis
>>> dis.dis(compile('[x for x in y]', '?', 'exec'))
  1           0 BUILD_LIST               0
              3 DUP_TOP
              4 STORE_NAME               0 (_[1])
              7 LOAD_NAME                1 (y)
             10 GET_ITER
        >>   11 FOR_ITER                13 (to 27)
             14 STORE_NAME               2 (x)
             17 LOAD_NAME                0 (_[1])
             20 LOAD_NAME                2 (x)
             23 LIST_APPEND
             24 JUMP_ABSOLUTE           11
        >>   27 DELETE_NAME              0 (_[1])
             30 POP_TOP
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE

请注意名称实际上必须再次删除!