文章目录
- [Python import 为什么有时候找不到模块------sys.path 与循环导入的死锁式排查](#Python import 为什么有时候找不到模块——sys.path 与循环导入的死锁式排查)
-
- 导入语
- [1 ~> import 的第一步------`sys.path` 决定 Python 去哪里找](#1 ~> import 的第一步——
sys.path决定 Python 去哪里找) -
- [1.1 import 搜索顺序](#1.1 import 搜索顺序)
- [1.2 著名的坑------自己的文件跟标准库重名](#1.2 著名的坑——自己的文件跟标准库重名)
- [2 ~> 包和模块------`init.py` 到底干啥](#2 ~> 包和模块——
__init__.py到底干啥) -
- [2.1 什么是包](#2.1 什么是包)
- [2.2 `init.py` 的作用](#2.2
__init__.py的作用) - [2.3 `init.py` 为空也不等于没用](#2.3
__init__.py为空也不等于没用)
- [3 ~> 相对导入------为什么有时能用有时不行](#3 ~> 相对导入——为什么有时能用有时不行)
-
- [3.1 语法](#3.1 语法)
- [3.2 最常见的翻车:脚本里用相对导入](#3.2 最常见的翻车:脚本里用相对导入)
- [4 ~> `name == "main"` 的作用](#4 ~>
__name__ == "__main__"的作用) -
- [4.1 一个我踩过的坑------相对导入 + `main`](#4.1 一个我踩过的坑——相对导入 +
__main__)
- [4.1 一个我踩过的坑------相对导入 + `main`](#4.1 一个我踩过的坑——相对导入 +
- [5 ~> 循环导入------两个文件互相 import](#5 ~> 循环导入——两个文件互相 import)
-
- [5.1 现象](#5.1 现象)
- [5.2 根因分析](#5.2 根因分析)
- [5.3 四种解决方案](#5.3 四种解决方案)
- [思考 && 总结](#思考 && 总结)
- 结尾
Python import 为什么有时候找不到模块------sys.path 与循环导入的死锁式排查
📖 文章简介: import 是 Python 中使用频率仅次于赋值的关键字,但大多数人对它背后发生的"模块搜索→加载→绑定名字"一系列步骤一知半解。本文从 sys.path 的优先级顺序讲起,逐层拆解七个场景:当前目录 vs 标准库同名冲突、相对导入的 .. 和 . 在脚本和包中的行为差异、__init__.py 到底是干什么的、__name__ == "__main__" 的真正作用、以及循环导入的死锁原理和四种解法。穿插真实经历------一个因为相对导入写错位置导致开发环境正常但生产环境报 ModuleNotFoundError 的坑。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
你一定遇到过这个错误------
ModuleNotFoundError: No module named 'xxx'
然后你去检查:包名没写错、文件确实在、__init__.py 也加了。为什么 Python 还是找不到?
import 的本质是模块搜索 ------Python 按一套固定的规则在磁盘上找 .py 文件,找到后加载执行并缓存。如果没找到或者加载过程中出现了循环导入,就报错。这套规则说起来就两条:sys.path 和相对导入。但是两者结合使用时,有些情况是真的反直觉。
1 ~> import 的第一步------sys.path 决定 Python 去哪里找
1.1 import 搜索顺序
python
import sys
for p in sys.path:
print(p)
典型的输出:
bash
'' # 空字符串 = 当前目录(或者脚本所在目录)
'/usr/lib/python3.10'
'/usr/lib/python3.10/lib-dynload'
'/home/user/.local/lib/python3.10/site-packages'
Python 启动时会自动把以下路径按顺序加入 sys.path:
- 当前目录 (脚本所在目录或
python -m的启动目录) - PYTHONPATH 环境变量(如果你配了)
- 标准库目录 (
/usr/lib/python3.x/) - 第三方库目录 (
site-packages/)
1.2 著名的坑------自己的文件跟标准库重名
python
# 你写了一个 random.py
import random
print(random.randint(1, 10)) # AttributeError: module 'random' has no attribute 'randint'
Python 在当前目录找到了你写的 random.py------它不是标准库的 random 模块。当前目录优先级最高。永远不要用标准库的名字给自己的 Python 文件命名。
2 ~> 包和模块------__init__.py 到底干啥
2.1 什么是包
bash
myproject/
├─ main.py
└─ utils/ ← 这是一个"包"
├─ __init__.py ← 有这个文件,utils 才被 Python 识别为包
└─ helper.py
2.2 __init__.py 的作用
它把一个目录从"普通文件夹"变成"包"。 Python 3.3 之后可以省略(隐式命名空间包),但绝大多数项目仍然保留它。
更重要的作用------导入包时自动执行它:
python
# utils/__init__.py
from .helper import clean_data # 从同级文件导入
# main.py
from utils import clean_data # 可以直接用,因为 __init__.py 帮你导出了
2.3 __init__.py 为空也不等于没用
如果没有 __init__.py,from utils import * 不会自动导入子模块。保留空 __init__.py 是最稳妥的做法。
3 ~> 相对导入------为什么有时能用有时不行
3.1 语法
python
from . import helper # 从当前包目录导入 helper
from .helper import clean # 从当前包的 helper 模块导入 clean
from .. import base # 从上一级包目录导入 base
from ..db import connection # 从上一级包的 db 模块导入
3.2 最常见的翻车:脚本里用相对导入
python
# helper.py(它自己是被直接启动的脚本)
from . import something # ❌ ImportError: attempted relative import with no known parent package
相对导入只在一个模块被当作"包内的一部分"导入时才有效,不能用于被直接执行的脚本。 这个限制常导致在 if __name__ == "__main__" 块里写相对导入的人卡住。
解法: 用绝对导入,或者用 python -m mypackage.helper 以模块方式运行。
4 ~> __name__ == "__main__" 的作用
python
# 这个文件名叫 helper.py
def do_something():
print("干活")
if __name__ == "__main__":
do_something()
- 当
python helper.py直接运行时:__name__="__main__",do_something()执行。 - 当
import helper被别的文件导入时:__name__="helper",do_something()不执行。
它的作用是让同一个文件在"直接运行"和"作为模块导入"两种场景下表现出不同行为------测试代码放这里最合适。
4.1 一个我踩过的坑------相对导入 + __main__
bash
myproject/
├─ app/
│ ├─ __init__.py
│ └─ main.py # 里面写了 from .utils import helper
└─ utils/
├─ __init__.py
└─ helper.py
python app/main.py 直接运行 → ImportError。因为 Python 把 main.py 当成顶级脚本运行,当前目录是 app/,相对导入找不到父包。
正确做法: 回到项目根目录,python -m app.main。
5 ~> 循环导入------两个文件互相 import
5.1 现象
python
# a.py
from b import b_func
def a_func():
return "A"
# b.py
from a import a_func
def b_func():
return "B"
运行 python a.py → ImportError: cannot import name 'b_func' from partially initialized module 'b'
5.2 根因分析
当 Python 执行 import b 时:
- 先去
sys.modules缓存中找------b 还没有 → 开始加载 b - b 进来第一行就
from a import a_func→ 又去找 a - a 此时还在初始化中(还没执行到
a_func的定义),于是报"partially initialized"
5.3 四种解决方案
方案一:延迟导入------把 import 移到用到的地方
python
# a.py
def a_func():
from b import b_func # 延迟到函数调用时才导入
return b_func()
方案二:导入模块而不是导入具体函数
python
# a.py
import b # 只导入模块,不导入名称
def a_func():
return b.b_func()
方案三:重构------把共享代码提取到第三个文件
python
# common.py(被 a 和 b 共同依赖)
def common_func():
pass
# a.py → from common import common_func
# b.py → from common import common_func
方案四:使用 sys.modules 直接引用
python
import sys
b = sys.modules.get("b") # 直接取已加载的模块引用
方案一和二解决了大部分日常场景。方案三是设计层面的根治------两个文件互相引用通常意味着职责边界没划清。
思考 && 总结
三条核心原则:
sys.path决定了搜索顺序。 当前目录优先级最高------这意味着不要用标准库名字给自己的文件命名。- 使用
python -m module_name运行自己的代码,避免相对导入问题。 在项目根目录以下python -m app.main最安全。 - 遇到 ImportError "partially initialized module",就是循环导入。 先查两个文件是不是互相引用,重构或延迟导入能解决。
结尾
各位小伙伴,import 的底层机制到这里拆完了。感谢阅读!
源码骑士 --- Python 全栈 & 系统架构
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!
🗡️ 寄语:技术之路,同行的人会让前路更有方向
结语:import 的问题说到底是"Python 去哪里找"和"找到之后加载时有没有互相卡住"两个问题。搞清 sys.path 和循环导入的原理,90% 的 import 问题都能秒杀。下篇是本系列的收官总结------一张图画完 Python 运行时内存模型。