为什么删除名为 __builtins__ 的全局变量只会阻止 REPL 访问内置函数?

Why does deleting a global variable named __builtins__ prevent only the REPL from accessing builtins?

我有一个包含以下内容的 python 脚本:

# foo.py

__builtins__ = 3
del __builtins__

print(int)  # <- this still works

奇怪的是,使用 -i 标志执行此脚本会阻止 REPL 访问内置函数:

aran-fey@starlight ~> python3 -i foo.py 
<class 'int'>
>>> print(int)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print' is not defined

为什么脚本可以访问内置函数,而 REPL 不能?

执行上下文不同。在 REPL 中,我们逐行工作 (Read, Eval, Print , Loop), 这允许在每个步骤之间更改全局执行范围的机会。但是执行模块的运行时是加载模块代码,然后在一个范围内执行。

在 CPython 中,通过在全局命名空间中查找名称 __builtins__ 来找到与代码块执行相关联的内置命名空间;这应该绑定到字典或模块(在后一种情况下使用模块的字典)。当在__main__模块中时,__builtins__是内置模块builtins,否则__builtins__绑定到builtins模块本身的字典。在您问题的两种情况下,我们都在 __main__ 模块中。

重要的是 CPython 仅在开始执行您的代码之前 一次 查找内置函数。在 REPL 中,每次执行新语句时都会发生这种情况。但是当执行 python 脚本时,脚本的全部内容是一个单元。这就是为什么删除脚本中间的内置函数没有效果。

为了在 REPL 中更紧密地复制该上下文,您不会逐行输入模块的代码,而是使用复合语句:

>>> if 1:
...     del __builtins__
...     print(123)
... 
123
>>> print(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print' is not defined

自然地,您现在可能想知道如何从脚本中删除内置函数。答案应该很明显:你不能通过重新绑定一个名字来做到这一点,但你可以通过突变来做到这一点:

# foo2.py
__builtins__.__dict__.clear()
print(int)  # <- NameError: name 'print' is not defined

作为最后的说明,__builtins__ 名称完全绑定的事实是 CPython 的 implementation detail,并且明确记录在案:

Users should not touch __builtins__; it is strictly an implementation detail.

不要依赖 __builtins__ 做任何严肃的事情,如果您需要访问该范围,正确的方法是 import builtins 然后从那里开始。

CPython 不会在每次需要进行内置变量查找时查找 __builtins__。每个框架对象都有一个 f_builtins 成员保存其内置变量 dict,内置变量查找从那里进行。

f_builtins 在创建框架对象时设置。如果一个新框架没有父框架f_back),或者一个与其父框架不同的全局变量dict,那么框架对象初始化查找 __builtins__ 以设置 f_builtins。 (如果新框架与其父框架共享一个全局字典,那么它将继承其父框架的 f_builtins。)这是 __builtins__ 参与内置变量查找的唯一方式。您可以在 _PyFrame_New_NoTrack.

中查看处理此问题的代码

当您在脚本中删除 __builtins__ 时,不会影响 f_builtins。在脚本的堆栈框架中执行的其余代码仍然可以看到内置函数。一旦脚本完成并且 -i 使您进入交互模式,每个交互命令都会获得一个新的堆栈帧(没有父级),然后重复 __builtins__ 查找。这是删除的 __builtins__ 终于重要的时候。