文章目录Python import 为什么有时候找不到模块——sys.path 与循环导入的死锁式排查导入语1 ~ import 的第一步——sys.path 决定 Python 去哪里找1.1 import 搜索顺序1.2 著名的坑——自己的文件跟标准库重名2 ~ 包和模块——__init__.py 到底干啥2.1 什么是包2.2 __init__.py 的作用2.3 __init__.py 为空也不等于没用3 ~ 相对导入——为什么有时能用有时不行3.1 语法3.2 最常见的翻车脚本里用相对导入4 ~ __name__ __main__ 的作用4.1 一个我踩过的坑——相对导入 __main__5 ~ 循环导入——两个文件互相 import5.1 现象5.2 根因分析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 搜索顺序importsysforpinsys.path:print(p)典型的输出# 空字符串 当前目录或者脚本所在目录/usr/lib/python3.10/usr/lib/python3.10/lib-dynload/home/user/.local/lib/python3.10/site-packagesPython 启动时会自动把以下路径按顺序加入sys.path当前目录脚本所在目录或python -m的启动目录PYTHONPATH 环境变量如果你配了标准库目录/usr/lib/python3.x/第三方库目录site-packages/1.2 著名的坑——自己的文件跟标准库重名# 你写了一个 random.pyimportrandomprint(random.randint(1,10))# AttributeError: module random has no attribute randintPython 在当前目录找到了你写的random.py——它不是标准库的 random 模块。当前目录优先级最高。永远不要用标准库的名字给自己的 Python 文件命名。2 ~ 包和模块——__init__.py到底干啥2.1 什么是包myproject/ ├─ main.py └─ utils/ ← 这是一个包├─ __init__.py ← 有这个文件utils 才被 Python 识别为包 └─ helper.py2.2__init__.py的作用它把一个目录从普通文件夹变成包。Python 3.3 之后可以省略隐式命名空间包但绝大多数项目仍然保留它。更重要的作用——导入包时自动执行它# utils/__init__.pyfrom.helperimportclean_data# 从同级文件导入# main.pyfromutilsimportclean_data# 可以直接用因为 __init__.py 帮你导出了2.3__init__.py为空也不等于没用如果没有__init__.pyfrom utils import *不会自动导入子模块。保留空__init__.py是最稳妥的做法。3 ~ 相对导入——为什么有时能用有时不行3.1 语法from.importhelper# 从当前包目录导入 helperfrom.helperimportclean# 从当前包的 helper 模块导入 cleanfrom..importbase# 从上一级包目录导入 basefrom..dbimportconnection# 从上一级包的 db 模块导入3.2 最常见的翻车脚本里用相对导入# helper.py它自己是被直接启动的脚本from.importsomething# ❌ ImportError: attempted relative import with no known parent package相对导入只在一个模块被当作包内的一部分导入时才有效不能用于被直接执行的脚本。这个限制常导致在if __name__ __main__块里写相对导入的人卡住。解法用绝对导入或者用python -m mypackage.helper以模块方式运行。4 ~__name__ __main__的作用# 这个文件名叫 helper.pydefdo_something():print(干活)if__name____main__:do_something()当python helper.py直接运行时__name____main__do_something()执行。当import helper被别的文件导入时__name__helperdo_something()不执行。它的作用是让同一个文件在直接运行和作为模块导入两种场景下表现出不同行为——测试代码放这里最合适。4.1 一个我踩过的坑——相对导入 __main__myproject/ ├─ app/ │ ├─ __init__.py │ └─ main.py# 里面写了 from .utils import helper└─ utils/ ├─ __init__.py └─ helper.pypython app/main.py直接运行 →ImportError。因为 Python 把main.py当成顶级脚本运行当前目录是app/相对导入找不到父包。正确做法回到项目根目录python -m app.main。5 ~ 循环导入——两个文件互相 import5.1 现象# a.pyfrombimportb_funcdefa_func():returnA# b.pyfromaimporta_funcdefb_func():returnB运行python a.py→ImportError: cannot import name b_func from partially initialized module b5.2 根因分析当 Python 执行import b时先去sys.modules缓存中找——b 还没有 → 开始加载 bb 进来第一行就from a import a_func→ 又去找 aa 此时还在初始化中还没执行到a_func的定义于是报partially initialized5.3 四种解决方案方案一延迟导入——把 import 移到用到的地方# a.pydefa_func():frombimportb_func# 延迟到函数调用时才导入returnb_func()方案二导入模块而不是导入具体函数# a.pyimportb# 只导入模块不导入名称defa_func():returnb.b_func()方案三重构——把共享代码提取到第三个文件# common.py被 a 和 b 共同依赖defcommon_func():pass# a.py → from common import common_func# b.py → from common import common_func方案四使用sys.modules直接引用importsys bsys.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 运行时内存模型。
09-Python模块导入机制-sys.path与循环导入的死锁式排查
文章目录Python import 为什么有时候找不到模块——sys.path 与循环导入的死锁式排查导入语1 ~ import 的第一步——sys.path 决定 Python 去哪里找1.1 import 搜索顺序1.2 著名的坑——自己的文件跟标准库重名2 ~ 包和模块——__init__.py 到底干啥2.1 什么是包2.2 __init__.py 的作用2.3 __init__.py 为空也不等于没用3 ~ 相对导入——为什么有时能用有时不行3.1 语法3.2 最常见的翻车脚本里用相对导入4 ~ __name__ __main__ 的作用4.1 一个我踩过的坑——相对导入 __main__5 ~ 循环导入——两个文件互相 import5.1 现象5.2 根因分析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 搜索顺序importsysforpinsys.path:print(p)典型的输出# 空字符串 当前目录或者脚本所在目录/usr/lib/python3.10/usr/lib/python3.10/lib-dynload/home/user/.local/lib/python3.10/site-packagesPython 启动时会自动把以下路径按顺序加入sys.path当前目录脚本所在目录或python -m的启动目录PYTHONPATH 环境变量如果你配了标准库目录/usr/lib/python3.x/第三方库目录site-packages/1.2 著名的坑——自己的文件跟标准库重名# 你写了一个 random.pyimportrandomprint(random.randint(1,10))# AttributeError: module random has no attribute randintPython 在当前目录找到了你写的random.py——它不是标准库的 random 模块。当前目录优先级最高。永远不要用标准库的名字给自己的 Python 文件命名。2 ~ 包和模块——__init__.py到底干啥2.1 什么是包myproject/ ├─ main.py └─ utils/ ← 这是一个包├─ __init__.py ← 有这个文件utils 才被 Python 识别为包 └─ helper.py2.2__init__.py的作用它把一个目录从普通文件夹变成包。Python 3.3 之后可以省略隐式命名空间包但绝大多数项目仍然保留它。更重要的作用——导入包时自动执行它# utils/__init__.pyfrom.helperimportclean_data# 从同级文件导入# main.pyfromutilsimportclean_data# 可以直接用因为 __init__.py 帮你导出了2.3__init__.py为空也不等于没用如果没有__init__.pyfrom utils import *不会自动导入子模块。保留空__init__.py是最稳妥的做法。3 ~ 相对导入——为什么有时能用有时不行3.1 语法from.importhelper# 从当前包目录导入 helperfrom.helperimportclean# 从当前包的 helper 模块导入 cleanfrom..importbase# 从上一级包目录导入 basefrom..dbimportconnection# 从上一级包的 db 模块导入3.2 最常见的翻车脚本里用相对导入# helper.py它自己是被直接启动的脚本from.importsomething# ❌ ImportError: attempted relative import with no known parent package相对导入只在一个模块被当作包内的一部分导入时才有效不能用于被直接执行的脚本。这个限制常导致在if __name__ __main__块里写相对导入的人卡住。解法用绝对导入或者用python -m mypackage.helper以模块方式运行。4 ~__name__ __main__的作用# 这个文件名叫 helper.pydefdo_something():print(干活)if__name____main__:do_something()当python helper.py直接运行时__name____main__do_something()执行。当import helper被别的文件导入时__name__helperdo_something()不执行。它的作用是让同一个文件在直接运行和作为模块导入两种场景下表现出不同行为——测试代码放这里最合适。4.1 一个我踩过的坑——相对导入 __main__myproject/ ├─ app/ │ ├─ __init__.py │ └─ main.py# 里面写了 from .utils import helper└─ utils/ ├─ __init__.py └─ helper.pypython app/main.py直接运行 →ImportError。因为 Python 把main.py当成顶级脚本运行当前目录是app/相对导入找不到父包。正确做法回到项目根目录python -m app.main。5 ~ 循环导入——两个文件互相 import5.1 现象# a.pyfrombimportb_funcdefa_func():returnA# b.pyfromaimporta_funcdefb_func():returnB运行python a.py→ImportError: cannot import name b_func from partially initialized module b5.2 根因分析当 Python 执行import b时先去sys.modules缓存中找——b 还没有 → 开始加载 bb 进来第一行就from a import a_func→ 又去找 aa 此时还在初始化中还没执行到a_func的定义于是报partially initialized5.3 四种解决方案方案一延迟导入——把 import 移到用到的地方# a.pydefa_func():frombimportb_func# 延迟到函数调用时才导入returnb_func()方案二导入模块而不是导入具体函数# a.pyimportb# 只导入模块不导入名称defa_func():returnb.b_func()方案三重构——把共享代码提取到第三个文件# common.py被 a 和 b 共同依赖defcommon_func():pass# a.py → from common import common_func# b.py → from common import common_func方案四使用sys.modules直接引用importsys bsys.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 运行时内存模型。