CPython 的解释器如何知道打印最后一个表达式的结果?

How does CPython's interpreter know to print the result of the last expression?

我一直在深入研究源代码,以弄清楚打印结果的位置。例如:

>>> x = 1
>>> x + 2
3

以上两条语句编译为:

  1           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (x)
              6 LOAD_CONST               1 (None)
              9 RETURN_VALUE

  1           0 LOAD_NAME                0 (x)
              3 LOAD_CONST               0 (2)
              6 BINARY_ADD
              7 RETURN_VALUE

第一条语句不打印任何内容,因为 None 是返回值。第二个returns相加的结果

CPython 的交互式循环调用 PyRun_InteractiveOneObjectEx() for each input. This gets the AST and invokes run_mod() to compile that AST to byte code and then evaluate the result in the virtual machine. The returned Python object that PyRun_InteractiveOneObjectEx() gets is simply the top of the VM's stack.

到目前为止,所有这些都是我所期望的。但是然后返回值好像是thrown away! REPL 何时打印?

顺便说一句,我可以看到交互模式确实改变了分词器;它 invokes PyOS_Readline 带有 sys.ps1 提示符(默认为 ">>> ")。我检查了 pythonrun.c 中的类似更改,但没有成功。

您正在展示通过在函数中包含代码生成的字节码的反汇编。这不是交互式代码的编译方式:它使用特殊的 'single' 模式(compile() 的第 3 个参数,如果您在 Python 代码中执行等效操作)。在这种模式下,丢弃每个表达式值的 POP_TOP 操作码变成了 PRINT_EXPRx = 1 什么都不打印的原因是语句在堆栈上没有留下任何需要弹出的东西,所以这个转换不适用。

正确!为了后代,这里是对正在发生的事情的更深入的了解。

以上反汇编显示eval模式的结果:

>>> list(compile('x + 2', '<stdin>', 'eval').co_code)
[101, 0, 0, 100, 0, 0, 23, 83]

可以通过以下方式查看操作码:

>>> import dis
>>> dis.opname[101]
'LOAD_NAME'
>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[23]
'BINARY_ADD'
>>> dis.opname[83]
'RETURN_VALUE'

操作码后面的两个数字表示一个 16 位操作数,尽管这里只需要第一个字节。所以这对应于:

LOAD_NAME     0
LOAD_CONST    0
BINARY_ADD
RETURN_VALUE

这些操作数分别是编译代码对象的变量名和常量池的索引。

>>> c = compile('x + 2', '<stdin>', 'eval')
>>> c.co_names
('x',)
>>> c.co_consts
(2,)

到目前为止,这就是我们的问题。但实际上,执行 Python 代码会导致:

>>> list(compile('x + 2', '<stdin>', 'exec').co_code)
[101, 0, 0, 100, 0, 0, 23, 1, 100, 1, 0, 83]
>>> dis.opname[1]
'POP_TOP'

即,结果被丢弃并引入 None 作为 return 值。

在交互模式下,我们有:

>>> list(compile('x + 2', '<stdin>', 'single').co_code)
[101, 0, 0, 100, 0, 0, 23, 70, 100, 1, 0, 83]
>>> dis.opname[70]
'PRINT_EXPR'

打印结果(通过 sys.displayhook)并且 None 成为实际的 return 值。

所以打印是由代码生成阶段引入的,而不是由 VM 引入的。