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

问题描述 投票:2回答:2

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

# foo.py

__builtins__ = 3
del __builtins__

print(int)  # <- this still works

奇怪的是,使用-i标志执行此脚本可以防止只有REPL访问builtins:

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不能?

python read-eval-print-loop built-in cpython
2个回答
2
投票

每次需要进行内置变量查找时,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__终于重要的时候。


2
投票

执行上下文不同。在REPL中,我们逐行工作(读取,评估,打印,循环),这使得全局执行范围有机会在每个步骤之间进行更改。但是执行模块的运行时是加载模块代码,然后在范围内执行。

在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,并且明确记录:

用户不应该触摸__builtins__;它严格来说是一个实现细节。

不要依赖__builtins__做任何严肃的事情,如果你需要访问那个范围,正确的方法是import builtins并从那里去。

© www.soinside.com 2019 - 2024. All rights reserved.