写这篇东西的起因,是我又双叒叕在群里看到有人发ModuleNotFoundError: No module named 'xxx'的截图了。说实话,这种问题对于刚接触 Python 和 PyCharm 的朋友来说,基本是必经之路。绝大多数情况下,报错信息里带着“No module named”字样,而且代码逻辑本身没毛病,那就是解释器找不到你的模块,跟代码写没写错关系不大。尤其是当你把项目拆成多个包、多个目录来管理代码时,模块不在同一个包下导致的导包失败会变得非常频繁,而且 Pycharm 有时候还会出现“明明是同一个项目,换个目录就导不了包”的诡异现象。
这篇东西我就围绕“模块不在同一个包中导致导包报错”这个场景,把原因、底层机制、解决方案和 Pycharm 里的坑一次性讲清楚。不管你是在校学生做课设,还是刚入职用 PyCharm 写业务代码,只要你被ImportError折磨过,这篇内容应该能帮你省下不少时间。
1. 先搞清楚 Python 导包的底层逻辑
1.1 “模块不在同一个包”到底是什么意思
很多人一看到导包报错,第一反应是去检查 import 语句有没有写错,或者文件名是不是拼错了。但实际上,当你确认代码本身没问题之后,问题往往出在“Python 解释器根本不知道去哪里找这个模块”。
这里需要先理清几个概念。模块就是一个.py文件,比如utils.py。包就是一个包含__init__.py文件的目录,从 Python 3.3 开始,即便没有__init__.py,目录也可以作为命名空间包被导入,但为了规范,我还是建议你保留这个文件。当我们写from utils import helper时,Python 解释器会按照sys.path里记录的路径列表,一个一个去查找名为utils.py的文件。
所以出现ModuleNotFoundError,本质上就是sys.path列表里没有包含目标模块所在的目录,或者包含的目录不对。所谓“模块不在同一个包中”,只是一个比较笼统的说法,实际场景可以细分成三种:
- 你要导入的模块在项目的另一个目录层级里,比如
src/module_a.py要导入src/core/module_b.py。 - 你要导入的模块在项目根目录之外,比如引入了外部的公共库或者另一个独立项目。
- 你要导入的目录本身被 PyCharm 错误地识别成了“普通目录”而不是“源根目录”(Sources Root),导致搜索路径没有自动加上。
我自己最常遇到的,是第一种和第三种叠加出现的情况:明明 py 文件就在项目里,打开终端运行却能跑通,在 PyCharm 里一点运行就报错。这种灵异现象,十有八九就是 PyCharm 的路径标记和终端下的路径不一致导致的。
1.2 sys.path 的搜索顺序,你用对了吗
先做一个简单实验。你在 PyCharm 里新建一个项目,随便写一个脚本,然后执行:
import sys for p in sys.path: print(p)你会看到类似这样的一组路径:
/Users/你的用户名/项目目录 /usr/local/lib/python3.9/site-packages /Library/Developer/CommandLineTools/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9 ...第一条路径通常就是项目根目录,或者你当前脚本所在的目录,PyCharm 会自动帮你加进去。但注意,这个“自动加进去”是有条件的,取决于 PyCharm 对目录的标记。
sys.path的搜索顺序大体是:
- 当前执行脚本所在的目录。
PYTHONPATH环境变量里记录的路径。- Python 标准库目录。
- site-packages 中安装的第三方库路径。
.pth文件里记录的路径。
平时你写import numpy,就是靠第 4 条路径找到的。而你想import config,如果你自己的config.py不在上述任何一条路径里,解释器就只能干瞪眼,直接给你抛一个ModuleNotFoundError。
那么“模块不在同一个包中”这个场景,通常就是第一条和第二条路径没覆盖到位。换句话说,要么你运行脚本的目录层级不对,要么项目根目录没有被正确标记为源根,要么PYTHONPATH没有配置。
2. 拆解三类最常见的 PyCharm 导包报错场景
2.1 场景一:同一项目,不同包之间的相互导入
这是我在处理问答时遇到最多的场景。假设项目结构如下:
project/ ├── main.py ├── utils/ │ ├── __init__.py │ └── helper.py └── services/ ├── __init__.py └── user_service.py现在user_service.py里想导入utils/helper.py的函数:
from utils.helper import format_name直觉告诉我,这个写法是对的。但你运行user_service.py时,可能会遇到两种结果:
- 如果直接右键运行
user_service.py,PyCharm 会把services目录加入sys.path,同时项目根目录project也被标记为 Sources Root,所以from utils.helper能正常找到。 - 但是如果
utils目录没有被正确标记,或者 PyCharm 没有把项目根目录加入路径,就会报错。
这里的关键在于,PyCharm 在运行脚本时,会把脚本所在目录和标记为 Sources Root 的目录加入sys.path。所以你观察一下 PyCharm 的目录颜色,正常的源根目录应该是蓝色。如果你看到utils或project显示为普通灰色目录,说明没有被标记,那么解释器就不会把它加入搜索路径。
我自己的建议是,在 PyCharm 里看到任何报ModuleNotFoundError且你自己确认 import 写法没问题的情况,先按住 Ctrl 键(Mac 上用 Cmd)点击 import 后面的模块名,看看能否跳转过去。如果跳转不了,就说明路径没被解释器识别,这时候再去检查目录标记。
2.2 场景二:跨项目或外部目录导入
还有一种场景比较折腾,就是你要导入的模块根本不在当前项目目录下。比如你在做接口自动化测试,公共的封装库放在另一个项目里,或者从同事那里拉下来的公共代码放在硬盘的某个公共目录里。
这种情况下,即便你把目录标记成 Sources Root,可能也没法解决问题,因为 PyCharm 的 Sources Root 是基于当前项目视图内的目录来标记的。如果你要把项目外部的目录加进来,需要另想办法,这个后面会详细说。
2.3 场景三:运行时终端与 PyCharm 运行结果不一致
这个问题特别迷惑人。很多人在 PyCharm 里运行报错,但是打开 macOS 的 Terminal 或者 Windows 的 CMD,手动进到项目目录再运行同一个文件,发现一切正常。
原因很简单:在终端里运行python main.py时,当前工作目录就是project,Python 解释器默认把当前工作目录加入sys.path,所以能搜到同级的utils。而在 PyCharm 里,默认的工作目录和sys.path的注入方式跟终端不完全一致,它依赖项目设置里的根目录标记。如果你某些目录没标记对,就会出现“终端能跑,IDE 里跑不了”的尴尬局面。
理解这个原理之后,就明白解决方案的核心思路:想办法让 PyCharm 运行脚本时,把目标模块的根目录注入sys.path。
3. 彻底解决:从包结构梳理到 PyCharm 配置
3.1 按照包的标准结构组织你的项目
如果项目是刚起步,还没有历史包袱,强烈建议一开始就按标准的包结构来组织目录。这样能从根本上避免大部分导包问题。一个合格的包结构通常长这样:
project/ # 项目根目录 ├── README.md ├── requirements.txt ├── setup.py # 可选,如果要打包发布 ├── my_package/ # 主包目录 │ ├── __init__.py │ ├── core/ │ │ ├── __init__.py │ │ └── engine.py │ ├── utils/ │ │ ├── __init__.py │ │ └── logger.py │ └── models/ │ ├── __init__.py │ └── user.py ├── tests/ # 测试目录 │ ├── __init__.py │ └── test_user.py └── scripts/ # 可执行脚本目录 ├── __init__.py └── run_demo.py整个项目的根目录是project,所有源码都放在一个主包my_package下面。这样,只要project被加入sys.path,你在scripts/run_demo.py里就可以用绝对导入:
from my_package.utils.logger import setup_logger from my_package.core.engine import Engine所有导入都从主包一层一层找下去,不会出现需要sys.path.append或者各种“跳目录导入”的别扭写法。这是我目前最推荐的项目组织方式,简单、清晰、可维护。
不过现实是很多老项目或者临时脚本并不会按照这种结构组织。没关系,下面这些方案同样能解决。
3.2 把项目根目录标记为 Sources Root
这是一个最直接的 PyCharm 操作,也是很多人踩坑最多的点。
在 PyCharm 左侧的项目文件树中,找到你想要作为导入根目录的文件夹(通常是项目根目录、src目录或者某个公共代码目录),右键点击它:
- 选择
Mark Directory as->Sources Root。
标记成功后,这个目录会变成蓝色,PyCharm 在运行时就会把这个目录加入sys.path。
比如在刚才的场景中,project下面有utils和services,你想在services里通过from utils.helper import xxx导入,那么你应该把project标记为 Sources Root。之后运行services下的任何文件,project目录都会被纳入搜索路径,Python 就能沿着project/utils/helper.py找到目标模块。
注意,这一步跟当前运行文件在哪里没有关系。有些同学会遇到一种情况:把services目录设置成 Sources Root 了,然后运行services里的文件,结果仍然找不到utils。这是因为如果你只把services标记为 Sources Root,解释器搜索的是services目录以及它下面的子模块,没有把上一级的project目录加进去。
所以操作时先想明白:你 import 语句里的那个“顶级包”或者“顶级模块”,它所在的目录是哪一个,就把那个目录设成 Sources Root。
3.3 不要让包目录“隐形”
聊到这个,顺便说一个容易忽略的点。当你在某个目录下创建了一个__init__.py文件,PyCharm 会把这个目录视为程序包。这本身没问题,但是有些情况下程序包里的模块导入外部模块,会产生“相对导入还是绝对导入”的困惑。
假设目录结构是:
project/ ├── main.py └── pkg_a/ ├── __init__.py └── module_a.py如果main.py的内容是:
from pkg_a import module_a module_a.say_hello()然后module_a.py里又想导入同包下的另一个模块module_b,那么正确写法是:
from pkg_a.module_b import another_func或者用相对导入:
from .module_b import another_func如果module_a里写了from module_b import another_func,解释器会先在sys.path里找有没有module_b.py,结果没有,因为module_b不在sys.path记录的任何一个目录下,它是躺在pkg_a这个子目录里的。于是报错。
这种“在同包内导入同包模块,却用了不带包前缀的写法”的情况也极其常见。究其原因,是写代码的人混淆了“脚本”和“模块”的概念。
脚本是你直接用 Python 运行的入口文件,它所在的目录会加入sys.path。模块是被 import 加载的文件,它的定位依赖包路径,不会因为“你俩在同一个文件夹里”就被自动识别为可导入的对象。如果你希望一个.py文件能被其他包导入,请把它当作模块、放在包结构里来管理,而不是指望解释器把每个文件目录都加进搜索路径。
3.4 临时方案:修改 sys.path
如果你的项目结构已经一团糟,或者你要快速验证某个功能,不想折腾 PyCharm 的目录标记,那可以在代码里临时手动添加路径。
import sys import os # 获取当前文件的绝对路径的上一级目录 current_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(current_dir) sys.path.append(parent_dir) from utils.helper import format_name这段代码的意思很清楚:先把当前文件所在的上一级目录加入sys.path,然后再导入utils下面的模块。不少人会把这段代码写在文件最顶部。
但这里我必须泼一盆冷水:这个方案只能临时救急,千万别把它当成常规做法。原因至少有两点:
- 如果这种
sys.path.append到处出现,代码的可维护性会变得极差。每个文件都要写一行路径拼接逻辑,以后项目挪个位置、或者别人接手代码,根本不知道你为什么要往sys.path里塞这个目录。 - 它跟你使用的 IDE 或者运行方式强相关。今天你在 PyCharm 里改好了,明天换 VS Code 跑同样的代码,路径又可能变得不对劲。
所以,能用前两种方案(规范项目结构、标记 Sources Root)解决的问题,就不要用sys.path.append这种打补丁的方式。
4. 实战案例:从报错到跑通的全过程
4.1 一个典型的复现场景
为了说得更明白,我构造一个实际会报错的场景,然后一步一步把它修好。
你从网上下了一个开源项目,结构大概是这样:
awesome_project/ ├── examples/ │ └── demo.py ├── awesome/ │ ├── __init__.py │ ├── core.py │ └── utils.py └── README.md你在 PyCharm 中直接右键运行examples/demo.py,文件里的代码是:
from awesome import core from awesome.utils import load_config core.run()结果 PyCharm 控制台给你来一句:
ModuleNotFoundError: No module named 'awesome'奇怪了,awesome目录明明存在。为什么找不到?
原因就在于,PyCharm 在运行examples/demo.py时,会自动把examples目录作为脚本目录加入sys.path。同时,如果项目根目录awesome_project没有被标记为 Sources Root,解释器就不会把awesome_project加入搜索路径。于是awesome这个目录虽然在项目里存在,但 Python 解释器“看不见”它,因为它不在搜索路径上。
4.2 一步步排查和修复
这里我按实际排查的顺序走一遍:
第一步,先检查目录是不是没有__init__.py。如果awesome目录下没有任何__init__.py,那么 Python 3 的命名空间包机制可能还能勉强工作,但在 PyCharm 中配合某些虚拟环境时会偶发出问题。稳妥起见,确认awesome目录里有__init__.py,awesome的子目录如果有也要有。
第二步,检查目录标记。在 PyCharm 项目树中看awesome_project目录颜色,如果不是蓝色,右键Mark Directory as | Sources Root。
第三步,再看运行配置。点击顶部运行按钮旁边的下拉框,选择Edit Configurations,确认 Python 解释器选对,工作目录(Working directory)是否为awesome_project根目录。
第四步,如果以上都没问题还报错,就在sys.path里把实际路径打印出来,看看有没有包含awesome_project:
import sys for p in sys.path: print(p)如果确实没有,并且你不想依赖目录标记,可以在运行配置里设置环境变量PYTHONPATH,指定为你的项目根目录。
![注意] 提醒一点:设置PYTHONPATH是全局影响比较小的方案,但 PyCharm 的 Run Configuration 默认不会自动加载PYTHONPATH环境变量,你需要手动在环境变量字段里添加PYTHONPATH=项目根目录。
把上面几步做完,当我再次右键运行demo.py,控制台输出正常,不再有ModuleNotFoundError。
4.3 多种解决办法对照
为了方便以后查阅,我把最常用的几种解决方案放在一起,做个对比:
| 方案 | 适用场景 | 操作难度 | 推荐程度 |
|---|---|---|---|
| 规范项目结构,统一用绝对导入 | 新项目、刚起步 | 前期需要设计 | 强烈推荐 |
| Mark Directory as Sources Root | PyCharm 内运行 | 最简单,右键即可 | 非常推荐 |
设置PYTHONPATH环境变量 | 跨环境运行、命令行也要跑 | 中等 | 推荐 |
代码中sys.path.append | 临时调试、来不及改结构 | 一行代码 | 不推荐 |
用相对导入from . import xxx | 同一个包内部互相引用 | 简单但要小心入口 | 可用但需理解 |
关于相对导入,我再多说一句。很多新手在 PyCharm 里写from . import xxx或者from ..something import xxx,然后直接右键运行当前文件,然后报ImportError: attempted relative import with no known parent package。
这个问题并不是你的包结构错了,而是因为直接运行一个包内的模块时,Python 不知道这个模块的父包是谁。相对导入只对于“作为模块被加载”的场景生效,对于“作为脚本直接运行”的场景不生效。
如果你想用相对导入,那么你应该把包外的入口文件作为启动脚本。比如:
project/ ├── run.py └── my_package/ ├── __init__.py ├── models/ │ ├── __init__.py │ └── user.py └── services/ ├── __init__.py └── auth.pyauth.py里写了:
from ..models.user import User那么你不要直接右键运行auth.py,而应该在project根目录下创建run.py,并在里面写from my_package.services.auth import ...,再运行run.py。只有这时候auth.py才有了父包my_package,相对导入才能正确解析。
5. PyCharm 配置层面容易忽视的细节与坑
5.1 Run Configuration 的 Working Directory 带来的假象
PyCharm 的每个运行配置(Run/Debug Configuration)里都有 Working Directory 这个选项,它会决定你运行脚本时的当前工作目录。很多人以为修改这个目录就能解决导包问题,其实它只解决了文件读写的问题,比如打开一个相对路径的配置文件;对于模块导入不一定有帮助。因为导入依托的是sys.path逻辑,而不是当前工作目录。除非你运行的脚本正好在 Working Directory 下,Python 才把脚本所在目录加入sys.path。
在查一个导包问题时,我建议先看一眼 PyCharm 的 Run Configuration:
Script path是不是指向了正确文件?Python interpreter是不是当前虚拟环境?Working directory是不是项目根目录?
有一个实际案例:某同学在test目录下放测试脚本,想导入项目根目录下的src包,但 Run Configuration 里的 Working Directory 指向了test目录,导致找不到src。把 Working Directory 改回项目根目录之后,问题就消失了。有时候 PyCharm 的自动检测并不总是符合预期,养成检查这些配置的习惯很重要。
5.2 Python 解释器与虚拟环境不一致
另一个容易被忽略的点是,Python 解释器环境可能不是同一个。比如你在终端里执行python --version发现是 3.10,但在 PyCharm 里右下角显示的却是另一个路径的 3.9 解释器,这可能导致你安装的第三方包在 PyCharm 里全部找不到。
导包报错如果涉及第三方库,比如ModuleNotFoundError: No module named 'requests',那就不是因为包结构问题,而是解释器环境不对。先到 PyCharm 的Settings | Project | Python Interpreter里确认解释器路径,然后在下方列表里看是否安装了对应的包。如果没有,点击加号安装即可。
pycharm 提供的方案本质是把虚拟环境所需的依赖统一管理。我在搞爬虫、数据分析项目时,常会为每个项目单独建一个虚拟环境,因为不同项目的包依赖版本经常冲突,比如 A 项目需要pandas 1.x,B 项目可能已经用到pandas 2.x。统一装到全局环境里很容易把环境搞乱,某个包被升级之后,另一个项目的代码就瘫了。
5.3 缓存与索引问题
如果你已经正确设置了 Sources Root,并且 import 语句也很标准,运行仍然报错,那有可能是 PyCharm 的缓存没跟上。特别是当你最近大量移动过文件、改过目录名称、从版本控制工具里拉过代码分支,PyCharm 的索引可能还是旧状态。
这时候去菜单栏执行File | Invalidate Caches...,然后选择Invalidate and Restart。PyCharm 会清空本地缓存并重启,重新索引项目。很多莫名其妙的跳转失败、导入识别错误,在清理缓存之后会有明显好转。
还有一种情况是,你项目里的__init__.py文件可能被误删,导致 Python 不把这个目录当作包来对待。检查一下你的包目录下是否有__init__.py,如果没有就直接新建一个空文件放进去。注意,虽然 Python 3 支持命名空间包,但在 PyCharm 的某些检查环节和旧代码里,保留__init__.py仍然是最兼容的做法。
6. 从命令行运行到 PyCharm 运行,如何保持一致性
6.1 使用终端时路径为什么没问题
回到我前提到的那个迷思:为什么代码在终端里运行正常,在 PyCharm 里就报错?
因为终端里当你执行:
cd ~/awesome_project python examples/demo.pyPython 解释器会默认把执行脚本的目录examples加入sys.path,注意,不是当前目录awesome_project。这里有两种情况往往能跑通:
- 如果脚本里有
from awesome import ...,那么搜索路径里需要有awesome_project目录。在旧版 Python(比如 3.9 之前),如果你直接运行python examples/demo.py,Python 会自动把examples而非父目录加入sys.path,所以awesome_project不一定在路径里。 - 某些 Python 版本和运行方式下,通过
python -m examples.demo运行,会把当前目录加入sys.path,这样awesome_project就能被搜到。
所以别以为“终端里能跑就是代码没问题”,实际上可能是运气好,Python 版本差异或者入口方式刚好让你碰上了。
6.2 用 -m 参数运行模块,很多问题能自然消失
在包结构的前提下,推荐从项目根目录使用-m参数来运行模块,而不是直接指定脚本路径。
比如:
cd ~/awesome_project python -m examples.demo这样 Python 会把当前工作目录作为sys.path的第一项,examples和awesome都能被正确识别为顶级包。如果examples目录下有__init__.py并且demo.py里面用了相对导入,-m方式同样能保证相对导入可用。
在 PyCharm 的 Run Configuration 里,也可以通过修改运行方式来达成类似效果:把运行目标从script path改成module name,然后填examples.demo,再把 Working Directory 设置为awesome_project根目录。这样 PyCharm 的运行行为就跟上面命令行一致了。
6.3 统一根目录,别让入口文件散落太深
开发新项目时,尽量保持主入口文件在项目根目录层级不要太深。如果入口在五六层目录下面,每次运行都要保证那一连串的目录都被正确标记,而其中任何一个环节出错,报错信息就会很莫名其妙。如果入口必须放在深层目录,比如scripts/deploy/run_deploy.py,那就把scripts的父级根目录标记为 Sources Root,然后在代码里统一使用“从项目根开始的绝对导入”,而不是一层层..往上找。
举个例子,如果你的项目结构是:
project/ ├── src/ │ ├── __init__.py │ └── logic.py └── scripts/deploy/ └── run_deploy.pyrun_deploy.py里你要导入logic.py,我建议你在 Run Configuration 里把 Working Directory 和 Sources Root 都指到project,然后代码写:
from src.logic import process_data不要写:
from ...src.logic import process_data后者虽然可能在某些相对路径场景下能跑,但一旦运行入口换了位置就翻车。保持绝对导入能让你减少很多不必要的折腾。
7. 高频报错速查:对照这个表就能定位问题
整理了一个速查表,把我在实际开发和帮人看代码时遇到的高频导包问题归类放在一起。遇到报错时,可以先对照这个表排查,省得漫无目的地乱试。
| 报错信息 | 可能原因 | 排查方向 |
|---|---|---|
ModuleNotFoundError: No module named 'xxx' | 模块不在sys.path的任何目录下 | 检查是否标记 Sources Root、确认模块目录结构 |
ModuleNotFoundError: No module named 'requests' | 第三方库未安装或解释器环境不对 | 检查当前解释器、在Settings里安装包 |
ImportError: cannot import name 'yyy' from 'xxx' | 模块里没有你要导入的属性或函数 | 检查拼写、是否循环导入、是否导入的是旧版本缓存 |
ImportError: attempted relative import with no known parent package | 直接运行包内模块且使用相对导入 | 改用脚本入口,或用-m方式运行 |
ModuleNotFoundError: No module named '__main__.xxx' | 在包内模块里错误地引用了入口模块名 | 检查循环导入和相对导入的使用方式 |
| 终端能跑但 PyCharm 报错 | PyCharm 的目录标记或运行配置不一致 | 检查 Source Root、Run Configuration 的 Working Directory |
| PyCharm 能跑但命令行报错 | 命令行下没有PYTHONPATH或当前目录不对 | 设置PYTHONPATH或改用python -m运行 |
这个表被我贴过很多次,基本覆盖了 80% 的导包问题。每次看到报错先别急,按照表格定位到原因类别,再对症处理,成功率很高。
关于“循环导入”这里也想多说一句,它的报错形式通常是ImportError: cannot import name 'xxx' from partially initialized module。出现的原因是两个模块互相 import,比如 A 模块 import B 模块,B 模块又 import A 模块。Python 在加载 A 的过程中发现需要加载 B,B 又回头加载还没完全加载完的 A,于是找不到 A 中尚未定义的变量。这种问题的解决办法通常是:
- 把公共函数或常量提取到一个新的模块里,两个模块都去 import 它。
- 把某个 import 语句放到函数内部,延迟到真正需要时再导入。
很多导包问题跟“不在同一个包”没直接关系,但同样会让人误以为是路径问题。如果你在排查时方向错了,就会浪费很多时间。
8. 自己踩过的坑和总结
坦白讲,从刚学 Python 到现在,我在导包这件事上交过的学费不在少数。印象最深的是一次爬虫项目,代码写到一半发现爬虫模块和解析模块放在两个包下,当时图省事到处写sys.path.append,结果代码跑得歪歪扭扭,后来项目要部署到服务器,那些硬编码的本地路径瞬间全部失效。最后花了大半天把项目结构整个重构了一遍,统一成标准包结构,删掉所有sys.path.append,问题才彻底根治。
从那以后,我再创建 PyCharm 项目时,都会先花几分钟规划目录结构,顺手把目录标记和解释器配置好,而不急着写代码。代码写到一半再回头调整结构,改起来比一开始就做对要麻烦得多。
还有一个小技巧是,如果项目里存在多个包需要互相同引用,建议在项目根目录放一个__init__.py。哪怕这个文件是空的,也能让 PyCharm 把所有包纳入同一个顶层命名空间下,检查代码时的识别率会提高不少。
关于 PyCharm 的目录标记,我再补充一个细节。PyCharm 的 Mark Directory as 不止有 Sources Root,还有 Test Sources Root、Resources Root 等。如果你的测试文件要导入项目源码里的模块,把测试目录标记为Test Sources Root、把源码目录标记为Sources Root即可。这样跑 pytest 或者 unittest 时,源码目录可以被搜索到,测试目录也不需要重复加入sys.path。
补充一句关于多人协作的建议:如果项目是团队共同维护的,尽量把导入路径规范写进 README,或者提供一个统一的环境配置文件,让每个人打开项目后先做一次目录标记。否则,你本地跑得很欢,同事拉下来代码跑不通就开始浪费时间。更聪明的做法是用刚才说的标准包结构加python -m运行方式,这样无论谁拿到代码,只要在根目录执行命令就能正常工作,不依赖 IDE 的目录标记状态。
最后再分享一个检查思路。导包报错之后,不要只盯着报错信息本身,关键是把下面几件事一次性排查清楚:
- 当前 PyCharm 用的 Python 解释器是哪个?是不是项目虚拟环境?
- 项目要导入的目录有没有被正确设置为 Sources Root?
- 代码里有没有出现裸的相对路径或者硬编码的
sys.path.append? - PyCharm 的缓存是不是该清理了?
- 目录结构里的
__init__.py有没有缺失?
把这些问题挨个过一遍,绝大多数导包问题都能在十分钟内解决。剩下那些特别离奇的,多半是代码中循环导入或者命名冲突,这时候就得静下心来看实际报错的堆栈信息了。但这是另一个话题,等下次有空再展开聊。