bigywater.tech
Python · 89

__main__:脚本运行和模块运行有什么区别

if __name__ == "__main__": 几乎每个 Python 文件里都有,但它背后的含义远不止「防止导入时执行」。这篇用同一个文件的三种运行方式,把 __name__、__package__、sys.path[0] 的差异一项项摊开,再看 __main__ 引起的三个典型问题。

__name__ 都是 "__main__"
脚本和 -m 运行时相同,区别在 __package__ 与相对导入
执行两次
a.py 当脚本运行、又被 b.py 导入时,顶层代码跑了两遍
RuntimeError
macOS(spawn)下缺少 __main__ 保护时,multiprocessing 的结果

验证一:同一个文件,三种运行方式

pkg/tool.py import sys print("__name__ =", repr(__name__)) print("__package__ =", repr(__package__)) print("__spec__ =", None if __spec__ is None else "ModuleSpec(name=%r)" % __spec__.name) print("sys.path[0] =", repr(sys.path[0])) try: from .util import hi; print("相对导入 from .util import hi →", hi()) except ImportError as e: print("相对导入 →", type(e).__name__ + ":", e)
实测 · 本机执行输出
$ python pkg/tool.py                (当成脚本)
__name__      = '__main__'
__package__   = None
__spec__      = None
sys.path[0]   = '<工作目录>/pkg'
相对导入 → ImportError: attempted relative import with no known parent package
$ python -m pkg.tool               (当成模块)
__name__      = '__main__'
__package__   = 'pkg'
__spec__      = ModuleSpec(name='pkg.tool')
sys.path[0]   = '<工作目录>'
相对导入 from .util import hi → util.hi
$ python -c 'import pkg.tool'      (被 import)
__name__      = 'pkg.tool'
__package__   = 'pkg'
__spec__      = ModuleSpec(name='pkg.tool')
sys.path[0]   = ''
相对导入 from .util import hi → util.hi

python pkg/tool.py 是「脚本」方式:没有所属的包,相对导入直接失败,sys.path[0] 是 pkg/ 目录本身。python -m pkg.tool 是「模块」方式:Python 先按包结构找到它,__package__ 是 pkg、有完整的 __spec__,相对导入才能工作,sys.path[0] 是当前工作目录。两种方式下 __name__ 都是 "__main__",只有被 import 时才是模块的真名。包内的文件要用相对导入,就用 -m 启动。

验证二:同一个文件被执行两次

实测 · 本机执行输出
$ python a.py
[a.py] 开始执行, __name__ = '__main__'
  [b.py] 开始, 它 import a
  [a.py] 开始执行, __name__ = 'a'
  [a.py] 结束, __name__ = 'a'
  [b.py] 结束, a 是 a | a.__file__ 的文件名 = a.py
  [a.py] 结束, __name__ = '__main__'

a.py 作为脚本运行时,它在 sys.modules 里的名字是 __main__,不是 a。当 b.py 里 import a 时,Python 找不到名叫 a 的已导入模块,只好把 a.py 重新执行一遍(这次 __name__ 是 "a")——顶层代码执行了两次,而且得到两个互不相干的模块对象,里面的类、全局变量也各有一份。

验证三:pickle 记住的是 __main__.Point

实测 · 本机执行输出
$ python save.py
已保存, pickle 里记录的类路径: b'\x80\x05\x95#\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x05Point\x94\x93\x94)\x81\x94}\x94\x8c\x01x\x94K\x03sb.'
$ python load.py
AttributeError: module '__main__' has no attribute 'Point'

类定义在被当作脚本运行的文件里,它的模块名就是 __main__,pickle 也就把 __main__.Point 写进了字节流。换一个脚本去 load,新脚本的 __main__ 里没有 Point,得到 AttributeError。需要持久化的类,要放在可以 import 的模块里,别定义在入口脚本里。

验证四:spawn 与 __main__ 保护

实测 · 本机执行输出
$ python mp_bad.py   (没有 if __name__ == '__main__' 保护)
RuntimeError:
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
$ python mp_ok.py
子进程在跑
父进程结束, 默认启动方式 = spawn

本机 macOS 上 multiprocessing 默认的启动方式是 spawn:子进程是一个全新的解释器,它会把主脚本重新导入一遍来找到 target。如果主脚本顶层没有 if __name__ == "__main__": 保护,子进程导入它时又会去启动新进程——Python 检测到这种情况,直接抛出 RuntimeError,而不是无限套娃。(默认启动方式在别的系统上不同,这里只在 macOS 上验证。)

验证五:目录、zip 也能作为程序,runpy 可以在代码里模拟

实测 · 本机执行输出
$ python app/
app/__main__.py 被执行, __name__ = '__main__' , sys.path[0] = <工作目录>/app/
$ python -m app
app/__main__.py 被执行, __name__ = '__main__' , sys.path[0] = <工作目录>
== 六、runpy: 在代码里模拟 python 运行 ==
script_x: __name__ = '<run_path>'
script_x: __name__ = '__main__'

只要目录或 zip 里有 __main__.py,就能用 python 目录/ 或 python -m 包名 运行,里面的 __name__ 也是 "__main__"。runpy.run_path 默认把 __name__ 设成 "<run_path>",要模拟「直接运行」得显式传 run_name="__main__"。

分步演示:同一份代码,几种运行方式下的差别

每一项都是上面的实际输出
__main__ 的几种面貌
关键差异

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 multiprocessing、pickle、runpy
    • 没有验证:Linux 上 fork 启动方式下缺少保护的表现、python -m 运行子包/命名空间包的其他规则、Windows
    京ICP备2026067502号-1京公网安备11010502063487号