我有一个包含以下内容的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不能?
每次需要进行内置变量查找时,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__
终于重要的时候。
执行上下文不同。在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
并从那里去。