如果你正从编写简单的 Python 脚本转向结构化项目开发,你很可能在 Import 和目录结构上遇到如下困惑:
应该使用 src 目录结构还是 flat 扁平目录结构吗?
在 Python 3 中还需要 __init__.py 吗?
这篇文章就带大家让深入讨论下这个问题。
1. 绝对导入 vs. 相对导入
假设有如下的电商项目结构:
ecommerce/
├── __init__.py
├── database/
│ ├── __init__.py
│ └── queries.py
└── payments/
├── __init__.py
├── stripe.py
└── paypal.py
绝对导入
绝对导入使用从项目根目录开始的完整路径。
-
例子: 在
stripe.py中使用queries.py中的save_transaction函数。
# ecommerce/payments/stripe.py
# 绝对导入
from ecommerce.database import queries
def process_payment():
queries.save_transaction()
相对导入
相对导入使用点号(.)来表示基于 _ 当前 _ 文件的路径。
-
语法:
from . import module(当前目录) 或from ..subfolder import module(父级目录)。 - 例子:上述例子变为
# ecommerce/payments/stripe.py
# 相对导入
from ..database import queries
def process_payment():
queries.save_transaction()
核心结论
| 维度 | 绝对导入 (推荐) | 相对导入 (慎用) |
|---|---|---|
| 可读性 | 极高。一眼就能看出代码来源。 | 一般。在复杂层级中容易数错点号。 |
| 重构 | 较难。修改根文件夹名需全局替换。 | 较易。移动整个子包时内部引用不破裂。 |
| 脚本执行 | 正常运行。 |
报错。直接运行 python stripe.py 会失败。 |
实践建议
- 总是使用绝对导入(PEP 8 推荐), 如果你不清楚相对导入的机制,请不要轻易使用。
- 仅在编写大型框架且子包内部关系极其紧密时,才考虑使用显式相对导入。
- 绝对禁止使用 Python 2 时代的隐式相对导入。
2. flat 结构 Vs src 结构
这是开发者从 " 写脚本 " 转向 " 写工程 " 的分水岭。
结构对比
-
flat 结构:
my_project/ ├── pyproject.toml └── my_package/ # 源码就在根目录 └── module.py-
诱惑: 简单。打开终端直接输入
python就能导入。 -
风险: 测试污染。由于当前路径
.默认在sys.path里,即使你打包时漏掉了一个重要文件,你的本地测试依然会通过(因为它直接读取了文件夹里的文件)。由于当前目录在sys.path中,你会误以为代码已安装。这会导致你漏掉某些文件没打包,但本地测试却能通过的窘境。
-
诱惑: 简单。打开终端直接输入
-
src 结构 :
my_project/ ├── pyproject.toml └── src/ # 源码被包裹在 src 中 └── my_package/ └── module.py-
优势: 强制安装。通过增加一个
src/文件夹,物理上隔绝了 " 当前路径 " 和 " 源码路径 "。这逼迫你执行pip install -e .(可编辑安装),让 Python 像对待标准库一样对待你的代码。这确保了你的测试环境与用户安装后的生产环境完全一致。 -
整洁: 根目录只放配置文件,逻辑代码全部在
src下。
-
优势: 强制安装。通过增加一个
核心结论
| 维度 | 扁平结构 (Flat Layout) | Src 结构 (Src Layout) |
|---|---|---|
| 结构简图 | root/my_pkg/ |
root/src/my_pkg/ |
| 导入便捷性 | 极高。在根目录打开终端即可导入。 |
较低。需先安装或手动设置 PYTHONPATH。 |
| 测试准确性 | 存在隐患。可能误导入本地源码而非安装包。 | 极高。强制测试 " 已打包 " 的代码。 |
| 打包发布 | 容易意外包含无关文件(如 tests)。 |
天然隔离,打包过程更纯净、规范。 |
| 适用场景 | 小型个人练手项目、简单脚本。 | 专业库、大型应用、开源项目。 |
实践建议
-
如果你在写库 (Library): 务必使用 src 结构。这能确保用户通过
pip install获得的代码与你测试的代码完全一致。 -
避免 " 影子导入 ": 使用 src 结构可以防止你的项目无意中覆盖掉标准库。例如,如果你在 Flat 结构下有个文件叫
code.py,它可能会干扰 Python 自带的code模块。 -
配置配套: 使用 src 结构时,在
pyproject.toml中需要显式指定源码路径:
[tool.setuptools.packages.find]
where = ["src"]
-
测试分离: 建议将
tests/文件夹放在src/之外。这样测试代码就不会被错误地打包进你的发布版本中。
3. __init__.py 还需要吗?
虽然 Python 3.3+ 引入了 " 命名空间包 ",允许文件夹不包含 __init__.py,但在实际工程中,你仍然应该保留它。
为什么保留?
-
明确标识: 告知
pytest、mypy等工具这是一个完整的 Python 包。 -
作为 " 门面 " (Facade): 你可以在
__init__.py中定义快捷访问。
代码示例:
# src/mypackage/__init__.py
from .payments import process_payment # 暴露核心接口
# 这样用户只需输入:
# from mypackage import process_payment
# 而不需要输入冗长的:
# from mypackage.internal.subfolder.payments import process_payment
总结
- 导入: 选绝对导入。明确比隐晦好。
- 布局: 选 src 结构。它能帮你挡掉 90% 的打包与路径错误。
- init.py: 保留它。它是包的灵魂,也是定义 API 接口的最佳场所。