if __name__ == "__main__": 几乎每个 Python 文件里都有,但它背后的含义远不止「防止导入时执行」。这篇用同一个文件的三种运行方式,把 __name__、__package__、sys.path[0] 的差异一项项摊开,再看 __main__ 引起的三个典型问题。
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")——顶层代码执行了两次,而且得到两个互不相干的模块对象,里面的类、全局变量也各有一份。
$ 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 的模块里,别定义在入口脚本里。
$ 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 上验证。)
$ 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__"。
multiprocessing、pickle、runpypython -m 运行子包/命名空间包的其他规则、Windows