pip -e 报 ModuleNotFoundError '非包'错误?原因与解决
2025-03-30 14:10:38
本地包 pip list
可见,导入却报 ModuleNotFoundError: 'xxx' is not a package
?原因与解决
搞 Python 开发的时候,你可能遇到过这么个怪事:明明用 pip install -e .
把本地项目装成了可编辑包,pip list
也清清楚楚地显示着它,可真到别的脚本里去 import
它的时候,却冷不丁地跳出 ModuleNotFoundError
,有时候更气人的是,错误信息还带着一句 'xxx' is not a package
。
就像下面这个场景:
项目结构长这样:
- project
|- LLaVA-NeXT <-- 源码和 setup.py 在这
| |- llava <-- 真正的包目录
| | |- __init__.py
| | |- model
| | | |- builder.py
| | | └─ ...
| | └─ mm_utils.py <-- 假设这个文件存在
| └─ setup.py <-- pip install -e . 会用到
|
└- playground
└- models
└- script.py <-- 需要导入 llava 包的脚本
在 LLaVA-NeXT
目录下,你信心满满地执行了 pip install -e .
。检查 pip list
,也确实看到了类似 llava 1.7.0.dev0 /home/username/project/LLaVA-NeXT
这样的条目。
然后,你切换到 playground/models
目录,或者就在项目根目录 project
下,尝试运行 script.py
。这个脚本里有这么一行:
# script.py
from llava.model.builder import load_pretrained_model
# ... 后续代码
结果,运行的时候,Python 无情地抛出了错误: ModuleNotFoundError: No module named 'llava.mm_utils'; 'llava' is not a package
。
这就怪了,包明明装好了,路径也没错,怎么就找不到,还说 llava
不是个包呢?
问题根源分析
这个问题的核心,往往不在于包 没装上,而在于 Python 解释器在 执行 script.py
时,没能正确地 找到并识别 那个通过 -e
安装的 llava
包。
咱们得先捋捋 Python 的导入机制和 -e
(editable) 安装模式干了啥:
-
Python 如何找包? 当你写下
import xxx
或from xxx import yyy
时,Python 会按顺序在一系列目录里查找xxx
。这个“一系列目录”就是sys.path
列表。它通常包含:- 当前脚本所在的目录。
- 环境变量
PYTHONPATH
里指定的目录。 - Python 安装时自带的标准库目录。
site-packages
目录,第三方库(包括用pip install
安装的)通常安家于此。
-
pip install -e .
做了什么?-e
或者--editable
模式,并不会像普通安装那样把你的代码文件复制到site-packages
里去。它玩了个“花招”:它在site-packages
目录下创建一个特殊的链接文件(比如.egg-link
文件或者.pth
文件)。这个文件里记录了你项目源代码的实际位置(比如/home/username/project/LLaVA-NeXT
)。当 Python 查找包时,看到这个链接文件,就知道:“哦,要找llava
包,得去那个原始路径下找。”这样做的好处是,你直接修改源代码,不用重新安装,改动就能立刻在导入它的地方生效。
那么,问题出在哪?
最常见的原因是 执行上下文 和 sys.path
的优先级 问题。
- 执行起点不同,
sys.path
就不同 :如果你是从project/playground/models/
目录运行python script.py
,那么project/playground/models/
就会被加到sys.path
的最前面。如果是在project/
目录下运行python playground/models/script.py
,那么project/
目录会被加到sys.path
的前面。 - 干扰与覆盖 :
pip install -e .
安装的包,虽然链接到了site-packages
,理论上 Python 能找到。但如果在sys.path
中,有某个优先级更高的路径下,存在一个也叫llava
的东西(可能是一个同名的空目录,或者是一个没有__init__.py
的文件夹),Python 可能会先找到这个“假”的llava
。当它尝试从这个“假货”里导入子模块(比如model
或mm_utils
)时,发现根本不是个正经的 Python 包结构,于是就懵了,报出'llava' is not a package
这种看似矛盾的错误。它确实找到了一个叫llava
的东西,但不是它期待的那个能导入子模块的包。
具体到你的例子,你在 script.py
(位于 project/playground/models/
)里导入 llava
。虽然 llava
通过 editable 方式安装在 /home/username/project/LLaVA-NeXT
,并通过 site-packages
里的链接指向了这里。但 Python 在解析 from llava.model.builder import ...
时,可能因为 sys.path
的配置或者当前工作目录的原因,没能正确地把 /home/username/project/LLaVA-NeXT/llava
识别为那个要找的顶级包。
可行的解决方案
知道了大概原因,咱们就可以对症下药了。试试下面几种方法:
方案一:调整脚本执行的起始目录
这是最简单也挺常见的解决办法。
原理:
改变运行 Python 脚本时的“当前工作目录”(Current Working Directory, CWD)。如果从项目的根目录 (project/
) 来执行子目录里的脚本,那么根目录会被自动添加到 sys.path
的开头。这有助于 Python 更直接地“看到”LLaVA-NeXT
这个目录,进而可能更容易通过 site-packages
的链接找到里面的 llava
包。
操作步骤:
-
打开你的终端。
-
cd
到你的项目根目录,也就是project/
那个层级。cd /home/username/project
-
然后,用相对路径指定要运行的脚本:
python playground/models/script.py
效果: 很多情况下,仅仅是换个地方执行命令,就能解决这个导入问题。
方案二:确认虚拟环境一致性
有时候犯的是低级错误:安装包和运行脚本用了俩不同的 Python 环境。
原理:
pip install
(包括 -e
) 是把包安装到 当前激活 的 Python 环境的 site-packages
目录下的。如果你安装时用的是一个虚拟环境 venvA
,跑脚本时却不小心用了系统默认的 Python 或者另一个虚拟环境 venvB
,那 venvB
的 site-packages
里自然没有那个指向 llava
的链接,肯定 import
不了。
操作步骤:
-
检查安装时的环境: 回忆一下或者查看你执行
pip install -e .
时,终端提示符前面是不是有虚拟环境的标识(比如(myenv)
)。用which pip
和which python
命令确认当时用的是哪个 Python 解释器和 pip。 -
检查运行时的环境: 在你打算运行
script.py
的终端里,同样执行which python
。 -
确保一致: 必须保证运行
script.py
的python
和当初安装llava
包的python
指向同一个可执行文件(位于同一个虚拟环境或系统环境中)。-
如果安装时在虚拟环境(假设叫
llava_env
)里,运行前确保已激活它:# Linux / macOS source path/to/your/llava_env/bin/activate # Windows (cmd.exe) path\to\your\llava_env\Scripts\activate.bat # Windows (PowerShell) .\path\to\your\llava_env\Scripts\Activate.ps1
-
确认激活后,
which python
应该指向llava_env
里的 python。
-
安全建议:
强烈推荐为每个项目使用独立的虚拟环境(用 venv
或 conda
创建)。这能隔离项目依赖,避免版本冲突,也能减少这类环境搞混的问题。
方案三:利用 PYTHONPATH
环境变量(谨慎使用)
可以手动把包含 llava
包源代码的目录(或者其父目录)添加到 PYTHONPATH
环境变量里。
原理:
PYTHONPATH
是一个环境变量,它的值是一系列目录路径,Python 会把这些路径也加入到 sys.path
中。通过把 LLaVA-NeXT
的父目录(也就是 project
目录)加进去,等于告诉 Python:“嘿,找包的时候,也顺便去 project
目录底下瞅瞅。”
操作步骤 (以 Linux/macOS 为例):
-
在终端里设置
PYTHONPATH
。这可以只对当前终端会话生效:export PYTHONPATH="/home/username/project:$PYTHONPATH"
这条命令的意思是,把
/home/username/project
这个路径加到现有PYTHONPATH
的最前面(如果PYTHONPATH
原本没设置,效果就是设置它为这个路径)。加在前面是为了提高优先级。 -
设置完之后,在这个 同一个终端 里运行你的脚本:
# 可以在任何地方运行了,比如直接在 script.py 所在目录 cd /home/username/project/playground/models python script.py # 或者在 project 目录运行 # cd /home/username/project # python playground/models/script.py
注意 (Windows):
在 Windows 的 cmd.exe
中,用 set PYTHONPATH=C:\path\to\your\project;%PYTHONPATH%
。
在 PowerShell 中,用 $env:PYTHONPATH = "C:\path\to\your\project;" + $env:PYTHONPATH
。
警告:
- 过度依赖
PYTHONPATH
会让项目环境变得复杂和不可靠。别人拿到你的代码可能跑不起来,因为他们没有设置相同的PYTHONPATH
。 - 它会影响全局的 Python 环境(除非你只在特定脚本或虚拟环境激活脚本里临时设置),可能引发意想不到的包冲突。
- 优先考虑方案一或方案二 。
PYTHONPATH
通常作为最后的手段或者用于特定场景。
进阶使用:在代码内部修改 sys.path
作为 PYTHONPATH
的替代,你可以在 script.py
的代码 开头,动态地把需要的路径添加到 sys.path
。
# script.py
import sys
import os
# 计算出项目根目录的绝对路径
# 这里假设 script.py 在 project/playground/models/ 下
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
# project_root 现在应该是 /home/username/project
# 将项目根目录添加到 sys.path 的最前面
# 这样做是为了让 Python 优先考虑从项目结构中找包
# 对于 editable 安装的包,添加其源码所在的根目录通常不是必须的,
# 因为 site-packages 的链接应该能工作,但有时添加项目根目录能解决路径查找问题
# 或者,如果你确定问题是找不到 LLaVA-NeXT 本身,可以尝试添加它
# llava_next_dir = os.path.join(project_root, 'LLaVA-NeXT')
# if llava_next_dir not in sys.path:
# sys.path.insert(0, llava_next_dir)
# 或者,更常见的是添加项目根目录到 sys.path
if project_root not in sys.path:
sys.path.insert(0, project_root) # 用 insert(0, ...) 保证高优先级
# 现在可以正常导入了
from llava.model.builder import load_pretrained_model
# ... 后续代码
优点: 不依赖外部环境变量设置,代码自包含性更好一点。
缺点: 修改了 sys.path
,稍微有点“hacky”,并且路径计算可能因项目结构变化而出错,降低了代码的可移植性。只推荐在确实理解影响的情况下使用。
方案四:改进项目结构
如果你的项目会长期开发维护,考虑更规范的结构可能一劳永逸。
原理:
让项目结构更符合 Python 包的标准实践,使得导入关系更清晰,减少对执行路径的依赖。
可能的调整:
-
将
playground
作为llava
的一部分: 如果playground
里的脚本是专门用于测试或演示llava
功能的,可以考虑把它移到LLaVA-NeXT
目录内部,比如作为LLaVA-NeXT/examples
或者LLaVA-NeXT/scripts
。这样,里面的脚本使用相对导入(如果合适)或直接导入llava
会更自然。 -
创建 Workspace 结构: 如果
LLaVA-NeXT
和playground
是两个相对独立但又需要交互的部分,可以考虑使用像src
布局,并让两个部分都成为可安装的包。- project |- pyproject.toml <-- 或者 setup.cfg/setup.py, 用于管理整个工作区 (可选) |- src | |- llava_pkg <-- 改个名避免冲突? 或者保持 llava | | |- __init__.py | | |- ... | └- playground_pkg | |- __init__.py | |- models | | |- script.py | | └─ ... | └─ ... |- setup_llava.py <-- 单独的安装脚本 for llava └─ setup_playground.py <-- 单独的安装脚本 for playground
这种结构下,你可能需要分别安装
llava
和playground
(或者通过工作区工具一次性管理)。然后在script.py
里就能清晰地from llava_pkg import ...
。
好处: 结构清晰,依赖明确,更符合大型项目的管理方式。
缺点: 可能需要对现有结构做较大调整。
方案五:尝试重新安装
虽然听起来简单,但有时候就是安装过程出了点小问题。
原理:
清理掉可能存在问题的旧链接文件或状态,重新执行一次 -e
安装。
操作步骤:
-
确保你在正确的虚拟环境中。
-
先卸载:
pip uninstall llava
它会询问你是否删除在
site-packages
中的链接文件,确认即可。 -
cd
到LLaVA-NeXT
目录下。 -
重新执行安装:
pip install -e .
-
再次尝试运行你的
script.py
(建议配合方案一,从项目根目录运行)。
以上几种方法,通常总有一种能搞定这个烦人的 ModuleNotFoundError: 'xxx' is not a package
问题。从调整执行目录和检查环境开始,往往是最快见效的。