globals()和locals()

特性 globals() locals()
作用域 全局(模块级) 局部(函数或类内部)
返回内容 当前模块全局符号表的字典 当前局部符号表的字典
可修改性 可修改,直接影响全局变量 通常只读,修改返回的字典一般不直接影响局部变量
函数外部行为 返回全局变量字典 globals() 返回结果相同
主要用途 查看/修改全局变量,动态代码执行 调试,查看局部变量状态

globals()返回的是实际的全局命名空间字典,因此对它的修改会直接改变全局变量

locals()在函数内部时,通常返回的是局部命名空间的一个拷贝。因此,通过locals()返回的字典来修改局部变量,通常不会成功。若在函数外部调用locals(),则返回和 globals() 相同的字典。

exec()

exec()用于在一个相对独立的环境下动态执行 python 代码。

globals和locals

exec()有两个重要参数globalslocals,它们是exec()实现环境隔离的关键。简单说,它们是两个可读写的字典,exec()执行代码时会通过读写这两个变量来实现对全局变量和局部变量的访问、定义和修改。无论是使用全局/局部的变量、类、函数,还是在全局/局部声明变量、定义函数和类,都通过读/写globalslocals来实现。

globalslocals的区别似乎根本不是一个在全局作用域、另一个在局部作用域,而是globals更倾向只读,locals更像是供修改的临时作用域。如果同时提供globalslocals,那么所有全局变量和局部变量都只会更新到locals中。这提供了更精细的环境隔离,但也会带来一个问题(或者说 bug),就是类作用域下访问变量(比如调用装饰器)只会去globals中查找,于是就找不到了。因此,还是暂时只提供globals就好,够用就行。

global_vars = {}
local_vars = {}

exec("""
a = 1  # 更新到 locals 中
print(a)  # 正常访问

class B:
    b = a  # 这里会报错找不到 a
""", global_vars, local_vars)

而对于局部变量,python 在编译阶段就会创建,根本不会修改globalslocals

global_vars = {"__builtins__": {}}
local_vars = {}

exec("""
a = 1

def func():
    a = 2
""", global_vars, local_vars)

print(global_vars)  # 输出:{'__builtins__': {}}
print(local_vars)  # 输出:{'a': 1, 'func': <function func at 0x7faff8606040>}

如果不提供locals,那么locals默认会指向globals,也就是说globals失去了它的只读属性,不过无所谓,够用。但是如果两者都不提供,exec()就会直接修改当前上下文,就失去了隔离作用,这在大多数情况下是需要避免的。

如果自定义的globals没有提供__builtins__属性,那么exec()会将当前上下文的__builtins__赋值给globals__builtins__,这意味着执行的代码可以使用所有的 python 内建函数,也就拥有了访问当前计算机文件系统等一切权限,这通常是不安全的。因此,如果不能保证执行的代码可信,还是需要为globals提供__builtins__属性以限制代码权限:

import builtins

global_vars = {
    # 显式提供 __builtins__ 以限制代码权限
    "__builtins__": {
        "print": builtins.print,
        # 或者直接这么写也行
        # "print": print
    },
}

# 如果不提供 __builtins__,则代码可以访问所有的内建函数
# global_vars = {}

exec("...", global_vars)

还可以通过下面的方式从内建函数中排除一部分:

safe_builtins = __builtins__.__dict__.copy()  # 创建一个当前内建函数集合的浅拷贝
del safe_builtins['open']  # 排除 open 函数,禁止访问文件系统

global_vars = {
    "__builtins__": safe_builtins,
}
exec("...", global_vars)

模块共享

exec()执行的代码和当前代码共用一个 python 进程,这意味着exec()内外虽然有着独立的上下文变量,但是可以共享sys.modulessys.path

import sys

print(id(sys))
print(list(sys.modules)[-3:])
global_vars = {}

exec("""
import sys
import re

print(id(sys))
""", global_vars)

print(list(sys.modules)[-3:])

代码输出为:

140135142473376
['_bootlocale', '_distutils_hack', 'site']
140135142473376
['functools', 'copyreg', 're']