
在函数内使用 exec 执行含类型注解(如 optional)的类定义会报 nameerror,根本原因在于 exec 默认仅访问局部作用域,而类型注解在类体中求值时无法回溯到全局导入——需显式传入 globals() 和 locals()。
在函数内使用 exec 执行含类型注解(如 optional)的类定义会报 nameerror,根本原因在于 exec 默认仅访问局部作用域,而类型注解在类体中求值时无法回溯到全局导入——需显式传入 globals() 和 locals()。
在 Python 中,exec() 的行为高度依赖其执行上下文的命名空间(namespace)。当 exec(code) 在模块顶层调用时,它默认使用当前全局作用域(globals())作为代码的命名空间,因此能正确解析 from typing import Optional 等导入语句。但一旦将 exec 移入函数内部(如 def dummy(): exec(code)),情况就发生本质变化:
- 函数的局部作用域(
locals())初始为空(不含任何导入); -
exec(code)若未显式指定命名空间,则仅使用该空局部作用域执行; - 虽然
class User(BaseModel):语句看似“成功”,实则是因为类名User和基类BaseModel的引用发生在类定义阶段(此时BaseModel已被exec内部的from pydantic import BaseModel动态注入到局部命名空间),但关键点在于:类体内的类型注解(如x: Optional[str])是在类创建过程中、由 Python 解释器对__annotations__字典求值时才真正执行的——此时Optional必须已在当前作用域中可访问,而它并未被注入到函数的局部命名空间中。
对比 L = List[str] 可以成功,是因为该赋值语句在类定义之外,属于模块级执行流,exec 会按顺序逐行执行并更新局部命名空间,所以 List 导入后立即生效;但类体内的注解是延迟求值的,且发生在独立的命名空间环境中。
✅ 正确解决方案是显式传递命名空间:
code = """
from pydantic import BaseModel
from typing import Optional, List
L = List[str] # ✅ 在 exec 局部作用域中定义
class User(BaseModel): # ✅ 基类引用可用(因上文已导入)
x: Optional[str] # ✅ 注解现在也能解析
"""
def dummy():
# 关键:显式传入 globals() 和 locals()
# 注意:locals() 在 exec 后会更新,但必须传入初始字典
namespace = {}
exec(code, globals(), namespace)
# 若需在函数内使用生成的类,可从 namespace 中提取:
User = namespace['User']
return User
user_instance = dummy()(x="hello")
print(user_instance.x) # 输出: hello⚠️ 注意事项:
立即学习“Python免费学习笔记(深入)”;
- 不要直接使用
exec(code, globals(), locals())—— 因为locals()在函数中是只读快照,修改它不会影响实际局部变量,且在优化模式(-O)下可能返回不可变字典; - 推荐做法是创建一个干净的
dict(如namespace = {})作为局部命名空间,并在exec后从中提取所需对象; - 此问题在 Python 3.10+ 中更显著,因类型注解的运行时求值逻辑更严格(PEP 563、PEP 649 影响);
- 生产环境应避免动态
exec类定义;若需元编程,优先考虑types.new_class()或__set_name__等安全机制。
总结:exec 不是“魔法执行器”,而是严格遵循 Python 命名空间规则的工具。理解 globals()/locals() 的作用时机与限制,是安全使用动态代码执行的关键。


















