首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >if __name__ == '__main__' 是什么?Python 模块入口守卫的完整拆解

if __name__ == '__main__' 是什么?Python 模块入口守卫的完整拆解

原创
作者头像
用户6689525
发布于 2026-10-03 10:12:48
发布于 2026-10-03 10:12:48
710
举报

Python先给结论:if __name__ == '__main__' 不是"程序入口"的仪式感,它是一道防止模块被导入时误执行的闸门。不写它,最轻的后果是导入一个模块就顺手连了数据库、跑了三分钟的脚本;最重的后果是 Windows 上一跑 multiprocessing 直接无限递归启动子进程然后崩掉。

几乎每个 Python 文件里都有这一行,但很多写了几年 Python 的人说不清它到底在防谁。这篇文章把它防的 4 类事故、背后的运行机制、以及什么时候其实可以不写,一次讲清楚。

一、__name__ 到底是什么

__name__ 是 Python 给每个模块自动创建的一个内置变量,值是字符串。它不需要你定义,导入器在加载模块时会自动塞进去。

它的取值只有两种可能:

  • 模块被直接运行 → __name__ 等于 '__main__'
  • 模块被导入 → __name__ 等于模块的完整名字(比如 utils.db,带包名)

所以 if __name__ == '__main__' 这句话翻译过来就是:"我这次是被当主程序跑的,不是被人 import 的。"

看着简单,但坑都在这句话的反面。

二、两种运行方式下的取值实测

新建一个 demo.py:

代码语言:javascript
复制
# demo.py
print("我的 __name__ 是:", __name__)

def hello():
    print("hello")

if __name__ == '__main__':
    print("→ 走了主程序分支")
    hello()

直接运行:

代码语言:javascript
复制
$ python demo.py
我的 __name__ 是: __main__
→ 走了主程序分支
hello

被导入:

代码语言:javascript
复制
$ python -c "import demo"
我的 __name__ 是: demo

注意第二行:print 那句顶层代码照样执行了,但 if 里的分支没走。这就是全部秘密——守卫只保护 if 缩进里的代码,守卫外面的一切,只要模块被加载就会跑,不管你是运行它还是导入它。

很多人以为加了守卫就万事大吉,其实是把副作用代码写在了守卫外面。

三、坑 1:多进程直接崩(Windows/macOS 必现)

这是最严重的一个坑,而且是新手最容易撞上的。

看这段代码,看起来完全正常:

代码语言:javascript
复制
# 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)上一跑就炸:

代码语言:javascript
复制
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 检测到了这种套娃,主动抛错拦下来。

正确写法就是把"启动动作"关进守卫里:

代码语言:javascript
复制
# 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 一下,副作用全跑了

这个坑不报错,所以更阴。

代码语言:javascript
复制
# 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 文件读了、数据库连接建了、后台线程起来了。

正确做法是把副作用包成函数,或者塞进守卫:

代码语言:javascript
复制
# 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 和相对导入的行为完全不同:

代码语言:javascript
复制
python pkg/mod.py        # __name__ = '__main__',__package__ = ''(相对导入会失败)
python -m pkg.mod        # __name__ = '__main__',__package__ = 'pkg'(相对导入可用)

如果你在 mod.py 里写了 from . import helper,第一种跑法会报:

代码语言:javascript
复制
ImportError: attempted relative import with no known parent package

而第二种跑法正常。所以带包的模块,统一用 python -m 跑,别用文件路径跑。

顺带说一个相关的:给包加一个 __main__.py,就能 python -m pkg 直接运行整个包:

代码语言:javascript
复制
# 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 导入主模块 → 模块名 → ✅ → ❌(递归在此断开)

八、什么时候可以不写

不是所有文件都需要守卫。下面三种情况省掉也没事:

1. 纯定义文件:只有函数、类、常量,没有任何可执行语句(很多 __init__.py 就是这样);

2. 一次性脚本:确定永远不会被 import,也不会用多进程;

3. 包内被导入的模块:它本来就是给别人用的。

但只要有下面任意一条,就老老实实写上:

  • 用了 multiprocessing / ProcessPoolExecutor / torch.multiprocessing;
  • 文件顶层有任何 I/O、网络、数据库动作;
  • 文件既当脚本跑、又被别处 import(最常见的场景)。

小结

  • __name__ 是模块的内置变量,直接运行时为 '__main__',被导入时为模块名。
  • 守卫只保护 if 缩进里的代码,写在守卫外面的顶层语句,导入时也照样执行。
  • 多进程必须用守卫:Windows/macOS 的 spawn 模式会重新导入主模块,没守卫就会无限递归启动子进程。
  • 副作用绝不能放顶层:读文件、连数据库、起线程这些动作,要么懒加载,要么关进守卫。
  • 带包的模块用 python -m 跑,相对导入才不会崩。
  • pytest 是导入模块,守卫里的准备逻辑它拿不到;Jupyter 里 __name__ 恒为 '__main__'。

---

你有没有因为忘了写这行,踩过多进程递归或者"导入即连接数据库"的坑?评论区聊聊,我挑几个典型的补进这篇文章。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、__name__ 到底是什么
  • 二、两种运行方式下的取值实测
  • 三、坑 1:多进程直接崩(Windows/macOS 必现)
  • 四、坑 2:被 import 一下,副作用全跑了
  • 五、坑 3:python -m 和 python file.py 不是一回事
  • 六、坑 4:测试框架和 Notebook 里的 __name__
  • 七、一张表看清所有情况
  • 八、什么时候可以不写
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档