起点:一个真实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.pypython -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.PYPYTHON -M FOO
sys.path[0]foo.py 所在目录当前工作目录
相对导入❌ 失败(foo 不在包里)✅ 正常
包内子模块入口难写自然支持
myapp/
__init__.py
cli.py # from .utils import helper
utils.py
__main__.py
  • python myapp/cli.pyImportError: attempted relative import with no known parent package
  • python -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 模式下)

这个机制让 picklemultiprocessing 等需要”识别主程序”的库能正确工作。

生态里的实例

命令背后文件
python -m pip install xpip/__main__.py
python -m venv .venvvenv/__main__.py
python -m http.serverhttp/server.py(注意是模块不是包)
python -m json.tooljson/tool.py
python -m unittestunittest/__main__.py
python -m pdb script.pypdb.py(顶层模块)