起点:一个真实issue(bug)
$ python3 -m XXX login
No module named XXX.__main__; 'XXX' is a package and cannot be directly executed 原因:XXX是一个包,但没有 main.py。
python -m X究竟是在做什么
-m 标志让 Python 将目标当作可导入的模块定位并执行,其底层由 runpy._run_module_as_main 实现。
| 目标形态 | Python 的处理方式 |
|---|---|
单文件模块 foo.py | 直接执行 foo.py,并将 __name__ 设为 "__main__"。 |
包 foo/ (含 __init__.py) | 执行该包下的 __main__.py 文件;若不存在则报错。 |
子模块 foo.bar | 先导入父包 foo,然后执行 foo/bar.py。 |
关键点: 包本身不能直接被执行。
__init__.py仅在包被导入时运行,用于初始化;而__main__.py才是使用-m时的执行入口。在 Python 机制中,“导入”与”执行”是被严格区分的两个过程。
为什么不直接跑__init__.py?
- 任何人 import pkg 都会触发”主程序”逻辑
- init.py 通常做副作用最少的事(暴露 API、注册子模块),让它变成 CLI 入口违背单一职责
- 没法区分”我在被导入”和”我在被当作脚本运行” 所以 Python 设计上:
- init.py → 导入时执行
- main.py → -m 执行时执行
与__name__ == “main”的联系
行为对比
| 场景 | 命令 / 操作 | name 的值 | 结果 |
|---|---|---|---|
| 直接执行 | python foo.py 或 python -m foo | "__main__" | 触发 main() 执行 |
| 模块导入 | import foo | "foo" | main() 不执行 |
__main__.py:包级别的“入口”
对于 包 (Package) 而言,由于目录本身不是可执行代码,无法直接在 __init__.py 中通过 if __name__ == "__main__" 来实现类似的逻辑(因为 __init__.py 的定位是初始化脚本)。
- 功能补全:
__main__.py专门为包提供了“被当作主程序执行”的语义。 - 执行逻辑:当你运行
python -m package_name时,Python 会自动寻找并执行该包下的__main__.py文件。 - 一致性:这使得“文件夹”在行为上可以表现得像一个“单文件模块”一样,拥有自己的执行入口。
python foo.py vs python -m foo 的差异
很多人以为这俩等价,其实有重要区别: 举例:项目结构
| 维度 | PYTHON FOO.PY | PYTHON -M FOO |
|---|---|---|
sys.path[0] | foo.py 所在目录 | 当前工作目录 |
| 相对导入 | ❌ 失败(foo 不在包里) | ✅ 正常 |
| 包内子模块入口 | 难写 | 自然支持 |
myapp/
__init__.py
cli.py # from .utils import helper
utils.py
__main__.pypython myapp/cli.py→ImportError: attempted relative import with no known parent packagepython -m myapp.cli→ 正常工作python -m myapp→ 跑__main__.py这就是为什么大型项目几乎都用-m风格。
__main__.py 的标准写法
# myapp/__main__.py
from myapp.cli import main
if __name__ == "__main__":
main()
注意:
- 即使是
__main__.py,仍然建议写if __name__ == "__main__":守卫。因为有人会import myapp.__main__(少见但合法),守卫能避免重复执行。 - 不要在
__main__.py里塞业务逻辑,只做”参数解析→调用真正入口”。真正入口放在cli.py之类的地方,方便测试和复用。
一个有趣的角落:__main__ 是个真模块
执行 python -m foo 时,Python 会创建一个名叫 __main__ 的模块对象,把 foo/__main__.py 的代码塞进去运行。也就是说:
import sys
print(sys.modules["__main__"]) # <module '__main__' from '.../foo/__main__.py'>
print(sys.modules["foo.__main__"]) # 同一个对象(在 -m 模式下)这个机制让 pickle、multiprocessing 等需要”识别主程序”的库能正确工作。
生态里的实例
| 命令 | 背后文件 |
|---|---|
python -m pip install x | pip/__main__.py |
python -m venv .venv | venv/__main__.py |
python -m http.server | http/server.py(注意是模块不是包) |
python -m json.tool | json/tool.py |
python -m unittest | unittest/__main__.py |
python -m pdb script.py | pdb.py(顶层模块) |