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

时间:2018-02-12 18:23:20

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

我使用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是......

  

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

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

2 个答案:

答案 0 :(得分:5)

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

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

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

因为它是一个函数,实际上,你需要传入你作为参数循环的iterable。这是.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_argcount元组中的第一个co_varnames值:

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

每个迭代结果都被x压入堆栈,再次弹出并存储在STORE_FASTLOAD_FAST,然后再次使用LIST_APPEND加载到堆栈中。最后,_[1]从堆栈中获取top元素,并将其添加到堆栈中下一个元素引用的列表中,以便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 再次返回给调用方。

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

<listcomp>

所以这创建了一个函数对象,其函数名为y(对回溯很有帮助),加载iter(y),为它生成一个迭代器(道德等价于def <listcomp>(.0): _[1] = [] for x in .0: _[1].append(x) return _[1] <listcomp>(iter(y)) ,并调用以迭代器作为参数的函数。

如果您想将其转换为Psuedo代码,它将如下所示:

_[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))

与外部字节码一起,生成器表达式等同于:

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

* 实际上不再需要临时变量;它们用于最初的理解实现,但是this commit from April 2007将编译器移动到只使用堆栈,这已经成为所有3.x版本以及Python 2.7的标准。将生成的名称视为对堆栈的引用仍然更容易。因为不再需要该变量,所以我提交了issue 32836以删除它,并且Python 3.8及其后将不再将其包含在符号表中。

在Python 2.6中,您仍然可以在反汇编中看到实际的临时名称:

    @bot.command(pass_context = True)
async def mute(ctx, user_id, userName: discord.User):
    if ctx.message.author.server_permissions.administrator:
        user = ctx.message.author
        role = discord.utils.get(user.server.roles, name="Muted")
        await client.add_roles(user, role)
     else:
       embed=discord.Embed(title="Permission Denied.", description="You don't have permission to use this command.", color=0xff00f6)
       await bot.say(embed=embed)

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

答案 1 :(得分:3)

因此,实现list-comprehensions的方式实际上是通过创建一个代码对象,它有点像创建一次性使用匿名函数,用于范围目的:

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