)
runpy 模块深度解析Python 模块定位与执行的官方实现-m开关底层机制【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonrunpy 是 CPython 标准库中负责在不 import 的情况下定位并执行 Python 模块的模块它的最主要用途是支撑python -m module命令行开关让脚本能够借助模块命名空间而非文件系统被定位与运行。读完本文你将掌握run_module()与run_path()两个核心函数的完整参数语义、__main__命名空间背后的特殊全局变量规则以及-m、目录 / zip 包执行在 CPython 解释器与 runpy 源码 中的真实调用链。本文以官方参考文档 Doc/library/runpy.rst 为主体并对照 Lib/runpy.py、Modules/main.c 与 Lib/test/test_runpy.py 展开源码级验证。runpy 是什么定位并执行而非导入runpy 模块的名称由其两个动作构成locating定位与running执行。与import语句的关键区别在于runpy 在找到模块代码后直接在当前进程中执行它不会把模块作为可导入对象注册进sys.modules或仅在短暂时间内临时注册并随后还原因此调用返回后无法像正常导入那样再次引用该模块。官方文档在模块开头明确了两条使用边界这不是沙箱模块。所有代码都在当前进程内执行代码产生的一切副作用——例如被缓存的其他模块导入sys.modules中的条目、对全局状态、对sys.path的改动——在函数返回后依然存在。执行代码中定义的函数与类在 runpy 函数返回后并不保证能正常工作。因为这些对象绑定的是执行期间创建的临时模块命名空间该命名空间在临时模块对象被清理后可能随之失效。若此限制不可接受应优先考虑 importlib尤其importlib.import_module()而不是本模块。这一点也在测试中得到印证Lib/test/test_runpy.py顶部注释写明该测试无法安全地在当前进程内直接测试_run_module_as_main因为那会破坏真实的__main__模块通常是test.regrtest该代码路径交给test_cmd_line_script测试。runpy 对外只公开两个函数见Lib/runpy.py的__all__ [run_module, run_path]函数定位方式典型等价命令行run_module(mod_name, ...)标准导入机制PEP 302按模块名定位python -m modulerun_path(path_name, ...)文件系统路径定位python path/to/script、python dir_or_ziprun_module()按模块名执行模块或包run_module(mod_name, init_globalsNone, run_nameNone, alter_sysFalse)执行指定模块的代码返回该模块执行后产生的 globals 字典。模块代码先通过标准导入机制细节参见 PEP 302被定位再在一个全新的模块命名空间中执行。参数与命名空间规则mod_name必须是绝对模块名。若指向的是包而非普通模块runpy 会先导入该包然后执行包内名为__main__的子模块并返回其 globals 字典。这一包执行能力自 Python 3.1 起加入。init_globals可选字典用于在代码执行前预填充模块 globals。该字典本身不会被修改内部通过run_globals.update(init_globals)把键值复制进新命名空间但若其中定义了下列特殊全局变量会被run_module()强制覆盖。run_name可选参数用于控制__name__的取值默认None。alter_sys可选参数为真时修改sys.argv[0]与sys.modules[__name__]详见下文sys 模块的临时改动。执行前的特殊全局变量代码执行前下列五个特殊变量会被写入 globals 字典这是最小集合解释器实现还可能隐式加入其他变量变量取值规则__name__若run_name非None则取run_name否则若目标模块是包则取mod_name .__main__其他情况取mod_name__spec__按实际导入的模块设置保证__spec__.name恒为mod_name或mod_name .__main__绝不会是run_name__file__、__loader__、__package__基于 module spec 按导入系统的正常规则设置见 导入系统属性说明 中的模块属性一节__cached__的处理随版本变化Python 3.2 起设置PEP 31473.4 起借助 PEP 451 的 ModuleSpec 使其对以该方式运行的模块也能正确赋值并保证真实模块名始终可经由__spec__.name获得3.12 起设置__cached__、__loader__、__package__的行为被标记弃用替代方案参见importlib.machinery.ModuleSpec而到 3.15__cached__这一全局变量已不再被设置。当前仓库为 3.16 开发版见 Include/patchlevel.h 中PY_VERSION 3.16.0a0Lib/runpy.py 中_run_code函数设置的变量也正是__name__、__file__、__doc__、__loader__、__package__、__spec__六个不含__cached__run_globals.update(__name__ mod_name, __file__ fname, __doc__ None, __loader__ loader, __package__ pkg_name, __spec__ mod_spec) exec(code, run_globals)alter_sysTrue 的行为与线程安全问题当alter_sys为真时runpy 会把sys.argv[0]更新为__file__的值把sys.modules[__name__]更新为一个临时模块对象模块执行期间可见在函数返回前将两者恢复为原始值。需要特别注意对sys模块的这种改动并非线程安全。其他线程可能看到尚未初始化完成的模块或看到被改动过的参数列表。文档明确建议在涉及线程的代码中调用本函数时不要动sys即保持alter_sysFalse或者自行以导入锁串行化、委托给独立进程。对比源码可看到两个路径的差异run_module()在alter_sysFalse时走_run_code完全不触碰 sys在alter_sysTrue时才走_run_module_code后者利用上下文管理器_TempModule与_ModifiedArgv0见 Lib/runpy.py分别完成sys.modules的临时替换与sys.argv[0]的临时改写。底层定位逻辑_get_module_details无论run_module还是run_path模块定位都汇聚到 Lib/runpy.py 的_get_module_details其行为可从源码归纳出拒绝相对模块名以.开头的mod_name直接抛出ImportError(Relative module names not supported)先导入父包对形如pkg.mod的名字先__import__父包以区分父包缺失与子模块自身初始化失败并对已存在于sys.modules的模块发出RuntimeWarning提示行为可能不可预测调用importlib.util.find_spec()获取 spec找不到则报ImportError包的处理若 spec 表明它是包submodule_search_locations非空则递归转向mod_name .__main__若该子模块不存在且包已被导入则给出%ris a package and cannot be directly executed的错误namespace 包与无 loader 模块namespace 包loader 为None会被拒绝执行通过spec.loader.get_code(mod_name)取代码取不到代码对象同样报错。值得注意的错误提示细节当模块名以.py结尾导致查找失败时runpy 会贴心提示试试去掉.py再用模块名例如用mymod代替mymod.py这正是python -m场景中常见误用。run_path()按文件系统路径执行脚本、目录或 ziprun_path(path_name, init_globalsNone, run_nameNone)执行指定文件系统位置处的代码并返回结果的 globals 字典。与命令行中传给 CPython 的脚本名一样path_name可以是一个Python 源码文件一个编译后的字节码文件.pyc一个合法的sys.path条目即内含顶层__main__.py的 zip 文件或目录。两种执行路径的行为差异情形一直接执行脚本文件。代码在一个全新模块命名空间中直接执行__file__被设为path_name而__spec__、__loader__、__package__均被置为None。源码路径上runpy 先尝试用pkgutil.read_code读取已编译文件读不到再作为源码compile执行见_get_code_from_fileLib/runpy.py。情形二执行合法sys.path条目目录 / zip。该条目会被插入到sys.path的最前面然后借助更新后的路径查找并执行__main__模块。此处没有对指定位置缺少__main__模块时误命中sys.path上其他位置的既有__main__条目做特殊防护——因此如果目标 zip/目录里没有__main__.py解释器可能转而执行别处的__main__这是使用者需要自行规避的陷阱。执行 zip/目录时__main__是从该sys.path条目导入而不是直接执行因此__spec__会按导入的__main__模块正确设置__spec__.name恒为__main____file__、__loader__、__package__依据 module spec 正常设置。特殊全局变量变量取值规则__name__run_name非None时取run_name否则为字符串run_path__file__直接执行脚本文件时为path_name执行sys.path条目时按 spec 设置__spec__、__loader__、__package__直接执行脚本时为None执行sys.path条目时按 spec 正常设置sys 的改动是强制的与run_module不同run_path对sys的改动不可选因为执行sys.path条目时这些调整必不可少。它的全部动作包括可能修改sys.path插入再移除路径、把sys.argv[0]更新为path_name、以临时模块对象替换sys.modules[__name__]并在返回前还原全部修改。源码中可见其用try/finally包裹sys.path.remove(path_name)确保即使执行异常也会把临时路径清理掉。由于run_module中提及的线程安全限制在此同样适用在线程代码中使用run_path时要么用导入锁串行化调用要么把它委托给独立进程执行。命令行开关与 runpy 的对应关系runpy 就是两条核心命令行语法的Python 化等价物官方在 Doc/using/cmdline.rst 中为两侧互挂了seealso命令行形式底层机制runpy 等价 APIpython -m module模块命名空间定位run_module(mod_name)对应 cmdline.rst -m 一节python path/to/script文件系统路径定位run_path(path_name)对应 cmdline.rst创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考