Python 模块入口入门指南:if __name__ == '__main__' 到底在防什么?

简介: 几乎每个 Python 文件都有 if __name__ == __main__,但很少有人说清它在防什么。本文用可复现的代码讲透 4 类真实事故:多进程递归崩溃、导入即连数据库、-m 与文件路径的差异、pytest 拿不到准备逻辑,并给出什么时候其实可以不写。

先给结论: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 导入主模块 模块名 ✅ ❌(递归在此断开)

八、什么时候可以不写

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

  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__'。

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

相关文章
|
12天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
7928 15
|
10天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1739 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
11天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1736 11
|
9天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
24天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3789 10
|
19天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1995 1

热门文章

最新文章