Python 中 if __name__ == "__main__" 有什么作用?模块导入和脚本执行有什么区别?
简化版
if __name__ == "__main__" 用来判断当前文件是被直接执行,还是被其他模块导入。直接执行时 __name__ 等于 "__main__",导入时 __name__ 是模块名,所以这段判断常用来放命令行入口、调试代码或示例代码,避免导入模块时自动执行。
详细版
Python 文件既可以作为脚本运行,也可以作为模块导入。作为脚本运行时,解释器会把当前模块的 __name__ 设为 "__main__";作为模块导入时,__name__ 通常是包路径形式的模块名,例如 pkg.worker。
常见写法是:
def main():
print("run program")
if __name__ == "__main__":
main()
这样别人 import this_module 时,只会加载函数和类定义,不会自动跑主流程。面试回答要强调:顶层代码在导入时也会执行,所以模块里不要把副作用很重的逻辑直接写在顶层;入口逻辑应该放进 main(),再用 __main__ 判断保护起来。
完整版教学
一、为什么同一个 .py 文件有两种身份
Python 的一个 .py 文件既可以是“可直接运行的脚本”,也可以是“可被复用的模块”。如果没有身份区分,写在文件底部的测试代码、命令行逻辑、启动服务逻辑,在别人导入这个模块时也会执行,造成非常隐蔽的副作用。
比如下面这个文件:
# tools.py
def add(a, b):
return a + b
print(add(1, 2))
当你只是想 from tools import add 时,print(add(1, 2)) 也会执行。这显然不符合“导入只是复用函数”的直觉。if __name__ == "__main__" 就是用来把“被直接运行时才执行”的逻辑隔离出来。
二、__name__ 到底是什么
每个模块都有一组特殊属性,__name__ 是其中之一。模块被导入时,__name__ 通常等于它在导入系统里的名字;模块被当作入口脚本执行时,__name__ 被设置为 "__main__"。
可以用两个场景对比:
python tools.py
-> tools.py 的 __name__ == "__main__"
import tools
-> tools.py 的 __name__ == "tools"
如果是包里的模块:
from app.jobs import worker
-> worker.py 的 __name__ == "app.jobs.worker"
这个差异就是判断入口的依据。它不是魔法开关,而是解释器在创建模块对象时设置的普通模块属性。
三、为什么导入也会执行顶层代码
导入模块不是简单把文件内容“声明一下”。CPython 第一次导入模块时,会找到模块文件、创建模块对象、执行模块顶层代码、把模块放进 sys.modules 缓存。函数定义、类定义、赋值语句、打印语句都会在这个阶段执行。
import tools
|
v
查找模块文件
|
创建模块对象
|
执行 tools.py 顶层代码
|
放入 sys.modules
这解释了为什么顶层副作用危险。假设一个模块顶层直接连接数据库、启动线程、删除临时文件,那么每个导入它的人都可能触发这些动作。更好的方式是只在顶层放定义,把启动逻辑放到 main() 或显式函数里。
模块顶层适合放“定义”,不适合放“重副作用执行”;入口逻辑要用
__main__保护。
四、main() 函数为什么也重要
有些人会直接把入口代码写在 if __name__ == "__main__" 下面,这可以工作,但更推荐再包一层 main()。原因是 main() 可以被测试调用,可以清晰返回退出码,也能让变量作用域更干净。
def main(argv=None) -> int:
argv = argv or []
print("args:", argv)
return 0
if __name__ == "__main__":
raise SystemExit(main())
这个写法有两个好处:测试时可以直接测 main(["--dry-run"]),命令行执行时又能通过 SystemExit 把返回码交给系统。对于小脚本这可能显得正式,但对工程项目很有价值。
| 写法 | 优点 | 问题 |
|---|---|---|
| 顶层直接执行 | 最短 | 导入就触发副作用 |
只写 if __main__ | 能防导入执行 | 测试入口略不方便 |
main() + if __main__ | 清晰、可测、可返回码 | 多几行代码 |
五、和 python -m 有什么关系
python -m package.module 会把某个模块按脚本入口执行。此时模块的 __name__ 也会是 "__main__",但它仍然能保留包上下文,常用于运行包内模块,避免直接执行文件导致相对导入出问题。
例如:
python -m app.jobs.worker
比下面这种直接跑文件更稳:
python app/jobs/worker.py
直接运行包内文件时,模块可能不知道自己属于哪个包,相对导入容易失败。-m 让解释器按模块导入规则定位,再把它作为入口执行。很多命令行工具和测试工具都依赖这个机制。
六、和 import 缓存、重复执行的关系
普通导入会把模块缓存到 sys.modules,同一个解释器进程里再次导入通常不会重复执行顶层代码。但直接作为脚本运行时,它以 "__main__" 身份存在;如果这个脚本内部又被按模块名导入,可能出现同一个文件以两个模块身份加载的情况。
一个简化例子:
python app/worker.py
-> 加载为 __main__
worker.py 内部或其他地方 import app.worker
-> 可能再加载为 app.worker
这样模块级单例、全局状态、注册表可能出现两份。工程上避免这种问题的办法是用 python -m app.worker 运行包内模块,并减少顶层可变全局状态。
七、常见误区与追问
- 误区:导入模块不会执行任何代码。 导入会执行模块顶层代码,只是函数体内部不会在定义时执行。
- 误区:
if __name__ == "__main__"能提升性能。 它主要控制入口逻辑是否执行,不是性能优化工具。 - 误区:所有脚本都必须写这个判断。 纯一次性脚本可以不写;可复用模块、包内入口、需要测试的脚本更应该写。
- 追问:为什么推荐写
main()? 入口逻辑更清晰、变量作用域更干净,也方便测试和返回退出码。 - 追问:
python file.py和python -m pkg.module有什么区别? 后者按模块路径执行,包上下文更正确,相对导入更稳定。 - 追问:导入后为什么第二次不再执行顶层代码? 模块会缓存到
sys.modules,重复导入通常直接复用模块对象。 - 追问:脚本和模块双重身份会造成什么坑? 同一文件可能以
__main__和真实模块名加载两次,导致全局状态重复。
八、加强记忆
把这题记成“导入会执行顶层代码,所以入口要关门”。__name__ 是模块身份,直接运行时叫 "__main__",被导入时叫模块名;if __name__ == "__main__" 就是只在直接运行时打开入口。工程上再配合 main() 和 python -m,既能避免导入副作用,又能让包内模块、测试和命令行入口更稳。