先给结论:if __name__ == '__main__' 不是"程序入口"的仪式感,它是一道防止模块被导入时误执行的闸门。不写它,最轻的后果是导入一个模块就顺手连了数据库、跑了三分钟的脚本;最重的后果是 Windows 上一跑 multiprocessing 直接无限递归启动子进程然后崩掉。
几乎每个 Python 文件里都有这一行,但很多写了几年 Python 的人说不清它到底在防谁。这篇文章把它防的 4 类事故、背后的运行机制、以及什么时候其实可以不写,一次讲清楚。
一、__name__ 到底是什么
__name__ 是 Python 给每个模块自动创建的一个内置变量,值是字符串。它不需要你定义,导入器在加载模块时会自动塞进去。
它的取值只有两种可能:
- 模块被直接运行 →
__name__等于'__main__' - 模块被导入 →
__name__等于模块的完整名字(比如utils.db,带包名)
所以 if __name__ == '__main__' 这句话翻译过来就是:"我这次是被当主程序跑的,不是被人 import 的。"
看着简单,但坑都在这句话的反面。
二、两种运行方式下的取值实测
新建一个 demo.py:
# demo.py
print("我的 __name__ 是:", __name__)
def hello():
print("hello")
if __name__ == '__main__':
print("→ 走了主程序分支")
hello()
直接运行:
$ python demo.py
我的 __name__ 是: __main__
→ 走了主程序分支
hello
被导入:
$ python -c "import demo"
我的 __name__ 是: demo
注意第二行:print 那句顶层代码照样执行了,但 if 里的分支没走。这就是全部秘密——守卫只保护 if 缩进里的代码,守卫外面的一切,只要模块被加载就会跑,不管你是运行它还是导入它。
很多人以为加了守卫就万事大吉,其实是把副作用代码写在了守卫外面。
三、坑 1:多进程直接崩(Windows/macOS 必现)
这是最严重的一个坑,而且是新手最容易撞上的。
看这段代码,看起来完全正常:
# bad_mp.py
import multiprocessing as mp
def work(x):
return x * x
print("顶层代码执行了")
p = mp.Process(target=work, args=(10,))
p.start()
p.join()
在 Linux 上跑没事,在 Windows 和 macOS(Python 3.8+ 默认 spawn)上一跑就炸:
RuntimeError:
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
...
if __name__ == '__main__':
freeze_support()
原因要解释一下:Windows 没有 fork,用的是 spawn 模式——子进程要重新导入一遍主模块,才能拿到 work 这个函数。而导入主模块时,p.start() 这行顶层代码又被执行了一次,于是再启动一个子进程,子进程再导入、再启动……无限递归。
Python 检测到了这种套娃,主动抛错拦下来。
正确写法就是把"启动动作"关进守卫里:
# good_mp.py
import multiprocessing as mp
def work(x):
return x * x
if __name__ == '__main__':
mp.freeze_support() # 打包成 exe 时建议加上
print("只在主进程打印一次")
p = mp.Process(target=work, args=(10,))
p.start()
p.join()
子进程导入主模块时,__name__ 是模块名而不是 '__main__',所以 p.start() 不会被执行,递归就此断掉。
这个坑同样适用于 ProcessPoolExecutor、torch.multiprocessing、以及任何底层用了 spawn 的库。
四、坑 2:被 import 一下,副作用全跑了
这个坑不报错,所以更阴。
# report.py
import pandas as pd
# 模块级副作用:一导入就执行
df = pd.read_csv("big_data.csv") # 读 800MB 文件,耗时 30 秒
conn = create_engine("mysql://...") # 建立数据库连接
scheduler.start() # 起了一个后台线程
def get_summary():
return df.describe()
同事只想用你那个 get_summary() 函数,from report import get_summary 一敲——800MB 文件读了、数据库连接建了、后台线程起来了。
正确做法是把副作用包成函数,或者塞进守卫:
# report.py
import pandas as pd
_df = None
def get_df():
global _df
if _df is None:
_df = pd.read_csv("big_data.csv") # 懒加载,用到了才读
return _df
def get_summary():
return get_df().describe()
if __name__ == '__main__':
print(get_summary()) # 只有直接跑这个文件时才读文件
判断标准很简单:任何"有外部代价"的动作(读文件、连库、起线程、发请求、打印大段日志)都不该写在模块顶层。
五、坑 3:python -m 和 python file.py 不是一回事
同一个文件,两种跑法,sys.path 和相对导入的行为完全不同:
python pkg/mod.py # __name__ = '__main__',__package__ = ''(相对导入会失败)
python -m pkg.mod # __name__ = '__main__',__package__ = 'pkg'(相对导入可用)
如果你在 mod.py 里写了 from . import helper,第一种跑法会报:
ImportError: attempted relative import with no known parent package
而第二种跑法正常。所以带包的模块,统一用 python -m 跑,别用文件路径跑。
顺带说一个相关的:给包加一个 __main__.py,就能 python -m pkg 直接运行整个包:
# pkg/__main__.py
from .mod import main
if __name__ == '__main__':
main()
六、坑 4:测试框架和 Notebook 里的 __name__
- pytest:pytest 是导入你的模块来跑测试的,所以模块里
__name__是模块名,守卫内的代码不会跑。如果你把测试用的准备逻辑写进了守卫,pytest 跑的时候会拿不到。 - Jupyter / IPython:每个 cell 的
__name__都是'__main__',所以守卫里的代码会执行。这也是为什么在 Notebook 里import自己的模块调试时,行为跟在脚本里不一样。 exec(open('a.py').read()):__name__也是'__main__',守卫会走。
七、一张表看清所有情况
| 运行 / 加载方式 | __name__ 的值 |
顶层代码执行 | 守卫内代码执行 |
|---|---|---|---|
python a.py |
'__main__' |
✅ | ✅ |
import a |
'a' |
✅ | ❌ |
from pkg import a |
'pkg.a' |
✅ | ❌ |
python -m pkg.a |
'__main__' |
✅ | ✅ |
python -m pkg(有 __main__.py) |
'__main__' |
✅ | ✅ |
| pytest 导入模块 | 模块名 | ✅ | ❌ |
| Jupyter cell | '__main__' |
✅ | ✅ |
| 多进程 spawn 导入主模块 | 模块名 | ✅ | ❌(递归在此断开) |
八、什么时候可以不写
不是所有文件都需要守卫。下面三种情况省掉也没事:
- 纯定义文件:只有函数、类、常量,没有任何可执行语句(很多
__init__.py就是这样); - 一次性脚本:确定永远不会被 import,也不会用多进程;
- 包内被导入的模块:它本来就是给别人用的。
但只要有下面任意一条,就老老实实写上:
- 用了
multiprocessing/ProcessPoolExecutor/torch.multiprocessing; - 文件顶层有任何 I/O、网络、数据库动作;
- 文件既当脚本跑、又被别处 import(最常见的场景)。
小结
__name__是模块的内置变量,直接运行时为'__main__',被导入时为模块名。- 守卫只保护
if缩进里的代码,写在守卫外面的顶层语句,导入时也照样执行。 - 多进程必须用守卫:Windows/macOS 的 spawn 模式会重新导入主模块,没守卫就会无限递归启动子进程。
- 副作用绝不能放顶层:读文件、连数据库、起线程这些动作,要么懒加载,要么关进守卫。
- 带包的模块用
python -m跑,相对导入才不会崩。 - pytest 是导入模块,守卫里的准备逻辑它拿不到;Jupyter 里
__name__恒为'__main__'。
你有没有因为忘了写这行,踩过多进程递归或者"导入即连接数据库"的坑?评论区聊聊,我挑几个典型的补进这篇文章。