返回

pip -e 报 ModuleNotFoundError '非包'错误?原因与解决

python

本地包 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) 安装模式干了啥:

  1. Python 如何找包? 当你写下 import xxxfrom xxx import yyy 时,Python 会按顺序在一系列目录里查找 xxx。这个“一系列目录”就是 sys.path 列表。它通常包含:

    • 当前脚本所在的目录。
    • 环境变量 PYTHONPATH 里指定的目录。
    • Python 安装时自带的标准库目录。
    • site-packages 目录,第三方库(包括用 pip install 安装的)通常安家于此。
  2. 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。当它尝试从这个“假货”里导入子模块(比如 modelmm_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 包。

操作步骤:

  1. 打开你的终端。

  2. cd 到你的项目根目录,也就是 project/ 那个层级。

    cd /home/username/project
    
  3. 然后,用相对路径指定要运行的脚本:

    python playground/models/script.py
    

效果: 很多情况下,仅仅是换个地方执行命令,就能解决这个导入问题。

方案二:确认虚拟环境一致性

有时候犯的是低级错误:安装包和运行脚本用了俩不同的 Python 环境。

原理:

pip install (包括 -e) 是把包安装到 当前激活 的 Python 环境的 site-packages 目录下的。如果你安装时用的是一个虚拟环境 venvA,跑脚本时却不小心用了系统默认的 Python 或者另一个虚拟环境 venvB,那 venvBsite-packages 里自然没有那个指向 llava 的链接,肯定 import 不了。

操作步骤:

  1. 检查安装时的环境: 回忆一下或者查看你执行 pip install -e . 时,终端提示符前面是不是有虚拟环境的标识(比如 (myenv))。用 which pipwhich python 命令确认当时用的是哪个 Python 解释器和 pip。

  2. 检查运行时的环境: 在你打算运行 script.py 的终端里,同样执行 which python

  3. 确保一致: 必须保证运行 script.pypython 和当初安装 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。

安全建议:

强烈推荐为每个项目使用独立的虚拟环境(用 venvconda 创建)。这能隔离项目依赖,避免版本冲突,也能减少这类环境搞混的问题。

方案三:利用 PYTHONPATH 环境变量(谨慎使用)

可以手动把包含 llava 包源代码的目录(或者其父目录)添加到 PYTHONPATH 环境变量里。

原理:

PYTHONPATH 是一个环境变量,它的值是一系列目录路径,Python 会把这些路径也加入到 sys.path 中。通过把 LLaVA-NeXT 的父目录(也就是 project 目录)加进去,等于告诉 Python:“嘿,找包的时候,也顺便去 project 目录底下瞅瞅。”

操作步骤 (以 Linux/macOS 为例):

  1. 在终端里设置 PYTHONPATH。这可以只对当前终端会话生效:

    export PYTHONPATH="/home/username/project:$PYTHONPATH"
    

    这条命令的意思是,把 /home/username/project 这个路径加到现有 PYTHONPATH 的最前面(如果 PYTHONPATH 原本没设置,效果就是设置它为这个路径)。加在前面是为了提高优先级。

  2. 设置完之后,在这个 同一个终端 里运行你的脚本:

    # 可以在任何地方运行了,比如直接在 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 包的标准实践,使得导入关系更清晰,减少对执行路径的依赖。

可能的调整:

  1. playground 作为 llava 的一部分: 如果 playground 里的脚本是专门用于测试或演示 llava 功能的,可以考虑把它移到 LLaVA-NeXT 目录内部,比如作为 LLaVA-NeXT/examples 或者 LLaVA-NeXT/scripts。这样,里面的脚本使用相对导入(如果合适)或直接导入 llava 会更自然。

  2. 创建 Workspace 结构: 如果 LLaVA-NeXTplayground 是两个相对独立但又需要交互的部分,可以考虑使用像 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
    

    这种结构下,你可能需要分别安装 llavaplayground(或者通过工作区工具一次性管理)。然后在 script.py 里就能清晰地 from llava_pkg import ...

好处: 结构清晰,依赖明确,更符合大型项目的管理方式。
缺点: 可能需要对现有结构做较大调整。

方案五:尝试重新安装

虽然听起来简单,但有时候就是安装过程出了点小问题。

原理:

清理掉可能存在问题的旧链接文件或状态,重新执行一次 -e 安装。

操作步骤:

  1. 确保你在正确的虚拟环境中。

  2. 先卸载:

    pip uninstall llava
    

    它会询问你是否删除在 site-packages 中的链接文件,确认即可。

  3. cdLLaVA-NeXT 目录下。

  4. 重新执行安装:

    pip install -e .
    
  5. 再次尝试运行你的 script.py (建议配合方案一,从项目根目录运行)。

以上几种方法,通常总有一种能搞定这个烦人的 ModuleNotFoundError: 'xxx' is not a package 问题。从调整执行目录和检查环境开始,往往是最快见效的。