【问题标题】:What are these extra symbols in a comprehension's symtable?理解的符号表中的这些额外符号是什么?
【发布时间】:2018-02-12 18:23:19
【问题描述】:

我正在使用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 只会产生符号 [&lt;symbol 'x'&gt;, &lt;symbol 'y'&gt;]

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

SymbolTable 中的条目对应于源中的标识符。

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

【问题讨论】:

    标签: python python-3.x list-comprehension python-internals


    【解决方案1】:

    这两个名称用于将列表推导实现为单独的作用域,它们具有以下含义:

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

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

    因为它是一个函数,所以实际上,您需要传入要循环的可迭代对象作为参数。这就是.0 的用途,它是第一个隐式参数(所以在索引0 处)。您生成的符号表明确列出 .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',)
    

    表的第一个子表是一个带有一个名为.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_VALUE 将栈顶返回给调用者,同样是_[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
    

    因此,这将创建一个函数对象,函数名为 &lt;listcomp&gt;(有助于回溯),加载 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 将编译器移动到仅使用堆栈,这已成为所有 3.x 版本以及 Python 2.7 的规范。将生成的名称视为对堆栈的引用仍然更容易。由于不再需要该变量,我提交了issue 32836 将其删除,Python 3.8 及更高版本将不再将其包含在符号表中。

    在 Python 2.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
    

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

    【讨论】:

    • 我认为_[1]实际上并没有在当前代码中使用;我认为它只是在使用它的东西被更改为不需要它之后被卡住了。
    • @user2357112:不确定,那我得仔细研究一下symtable_handle_comprehension()compiler_comprehension() 的修订历史。然而,我有一个明显的印象,即临时名称是用来处理列表对象的。
    • @user2357112: bingo: this commit from 11 years ago 似乎已删除临时名称处理,但符号表仍会发出它们。我会在一分钟内更新(必须参加一些事情)。
    • @MartijnPieters 感谢您富有洞察力的回答和深入挖掘。现在我很抱歉我没有想到至少自己拆解字节码。
    【解决方案2】:

    所以,列表理解的实现方式实际上是通过创建一个代码对象,它有点像创建一个一次性使用的匿名函数,用于范围界定:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-11-23
      • 2021-02-13
      • 2016-10-13
      • 1970-01-01
      • 2020-11-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-01
      相关资源
      最近更新 更多