从零开始搭建Python项目:我的目录结构实践

📅 发布时间:2026/8/21 8:22:57
从零开始搭建Python项目:我的目录结构实践 把项目目录建对的那一刻代码还没写胜负已分一半。我见过太多Python项目在三个月后变得不可维护文件散落在根目录utils.py膨胀到两千行测试文件命名成test1.py、test2_final.py导入路径靠猜依赖管理靠记忆。这些问题几乎都能追溯到最初十分钟里那个草率的目录结构决定。今天我想和你分享我自己从零开始搭建Python项目时沉淀下来的一套目录结构实践——以及它背后的思考逻辑。先别急着建文件夹你需要一个“倒置”的视角很多教程会直接给你一张树状图让你照抄。但照抄的目录是死的理解结构背后的职责边界你才能真正驾驭它。我的做法是先从“项目将如何被使用”倒推目录。问自己三个问题这个项目是库library还是应用application还是两者兼有谁会运行它是其他开发者通过import调用还是用户通过命令行执行它要存活多久是原型验证还是打算维护五年这三个答案决定了顶层划分。如果是个库你需要一个干净的包名目录把实现细节藏进去如果是个应用你需要src布局和入口脚本如果两者兼有你要在src下同时放包和__main__.py。目录结构是项目契约的第一份书面表达写错了后面每一步都在还债。我踩过最大的坑是刚学Python时把所有模块直接放在根目录。当时觉得简单但一旦文件数量超过十个import语句开始互相纠缠循环导入和命名冲突接踵而至。后来我才明白根目录应该只留给项目元数据而不是业务代码——这是对Python包机制最基本的尊重。我的推荐结构src布局 明确的顶层职责以下是我目前实践下来最顺手的结构适用于大多数中小型Python项目库或应用皆可myproject/ ├── src/ │ └── mypackage/ │ ├── __init__.py │ ├── __main__.py │ ├── core.py │ ├── utils.py │ ├── models.py │ ├── services/ │ │ ├── __init__.py │ │ ├── auth.py │ │ └── payment.py │ └── cli.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_core.py │ └── integration/ │ └── test_workflow.py ├── scripts/ │ ├── setup_db.py │ └── backup.py ├── docs/ │ └── architecture.md ├── configs/ │ ├── dev.yaml │ └── prod.yaml ├── pyproject.toml ├── README.md ├── .gitignore ├── .env.example └── Makefilesrc目录是整个结构的核心。它的作用不是“多一层文件夹”而是强制你以“已安装包”的视角看待自己的代码。所有业务逻辑都放进src/mypackage/你的入口脚本比如python -m mypackage和测试代码都通过mypackage.core这样的绝对导入来引用。这避免了“本地导入”和“安装后导入”行为不一致的经典问题——很多人开发时跑得动打包后却报ModuleNotFoundError多半就是没用src布局。tests目录与src平级测试代码永远不该被打进生产包这是铁律。conftest.py放共享fixture测试文件名带test_前缀每个测试文件只测一个模块或一个功能域。我在tests里还分了一个integration/子目录专门放跨模块协作的测试与单元测试物理隔离这样在CI里可以分别运行快速定位失败层级。scripts目录放那些“不用进包但项目需要”的一次性或运维脚本。比如初始化数据库、定时备份。它的存在提醒我不要把脚本塞进utils.py里。你项目里的utils.py应该只包含纯函数而不是能直接执行的对外动作。configs目录集中管理不同环境的配置。注意不要把配置文件写死在代码里——用os.environ或pydantic-settings去读取环境变量然后从configs加载对应YAML。安全和可移植性从配置文件与代码分离开始。pyproject.toml是现代Python项目的“身份证”。它替代了老旧的setup.py、requirements.txt和setup.cfg用一份声明式的配置同时管理依赖、构建工具和项目元数据。我强烈建议你从今天起就切换到pyproject.toml这是Python打包生态的未来也是唯一值得长期维护的配置入口。包内部不要“一马平川”按领域分包按层组织很多人的src/mypackage/下面是平的main.py、helper.py、data_loader.py……文件多了以后依赖关系像蜘蛛网一样。我的做法是在包内部按领域或分层再划分二级包。比如services/下放业务服务models/下放数据模型api/下放接口层。如果项目还涉及复杂的数据库操作再加一个repositories/。分层的关键是“依赖单向”。上层可以依赖下层下层绝不能依赖上层。比如services可以import models但models里绝不应该import services。这个约束看起来简单却是防止架构腐化的第一道防线。我在代码评审时最先看的就是import的方向。只要发现一个“绕路”的导入就说明目录边界被打破了要立刻重构不能等。还有一点__init__.py别只写个空文件。它是包的对外接口面。你应该在__init__.py里显式导出该包要暴露的公共API。比如在mypackage/__init__.py里写from .core import create_app from .models import User, Order __all__ [create_app, User, Order]这样外部用户from mypackage import create_app而不是from mypackage.core import create_app。这层“门面模式”为重构提供了自由——只要__init__.py的导出不变包内部的模块怎么折腾外部都不受影响。良好的目录结构最终要服务于接口的稳定。入口设计与__main__.py让python -m成为标准一个项目总得有个“起点”。对应用而言我习惯在包内放一个__main__.py让我可以用python -m mypackage来启动程序。这个模块通常读命令行参数、加载配置、实例化核心对象、然后跑起来。它非常薄——入口模块只负责“组装”不负责“业务”。对库而言__main__.py不是必需的但如果你提供了一个CLI工具把命令挂到pyproject.toml的[project.scripts]入口上比写死if __name__ __main__更干净。比如[project.scripts] mycli mypackage.cli:main这样用户安装你的包后直接用mycli命令就能调用。把入口从“脚本”升级为“可安装命令”是项目从玩具走向产品的第一步。我在src/mypackage/cli.py里用argparse或click定义所有子命令每个子命令函数只做参数解析和调用对应的services层方法逻辑一清爽测试也好写。测试不是附属品它是目录结构的第一消费者如果你问我目录结构为谁服务我的答案不是“写代码的人”而是“运行测试的人”。可测试性是衡量目录结构是否优秀的黄金标准。我用一个最小化测试文件来约束自己的结构# tests/test_core.py import mypackage from mypackage.core import process def test_process(): assert process([1, 2]) 3注意这里import mypackage是直接导入不需要手动添加sys.path。这得益于src布局和pyproject.toml里的build-system配置。如果你还需要在测试里改sys.path或者用sys.path.append(../src)那你的目录结构一定有地方不对劲了。conftest.py是与测试同级的“隐形基础设施”。我在这里定义fixture比如临时数据库连接、mock对象、环境变量缓存。fixture的复用价值远大于复制粘贴几行setUp。我习惯把跨测试文件的共享fixture放在tests/conftest.py把仅限某个子目录用的fixture放在对应子目录里的conftest.py——这本身就是一种目录结构的层次化设计。脚本与自动化scripts和Makefile的配合scripts目录里的东西不是随便扔进去就完了。我用Makefile作为统一的命令入口把那些python scripts/setup_db.py、pytest、ruff check .之类的长命令封装成短目标install: pip install -e .[dev] test: pytest -q lint: ruff check src tests db-setup: python scripts/setup_db.py run: python -m mypackageMakefile的价值不在“构建”而在“记忆”。它把项目最常用的命令浓缩成几个字符减少团队协作时的口口相传。你或许会说“用taskipy或pre-commit不是更Pythonic吗”工具当然可以换但scripts目录本身作为一种“项目操作说明书”的位置是任何工具替代不了的。我见过有人把备份脚本放进src里结果每次打包都要额外排除还把非核心依赖带进了生产环境。凡是“操作项目”的都放scripts凡是“构成项目”的都进src。文档与配置别让它们无家可归README.md是门面docs/是仓库。我坚持在docs/下放“有结构”的文档比如架构说明、决策记录ADR、部署手册。文档的目录结构本身也反映你对项目的理解深度。我不喜欢把所有文档平铺在docs/里而是分architecture/、decisions/、operations/。这跟代码分层一样——按主题聚合而不是按时间追加。配置方面我除了configs/目录还特别重视.env.example。这个文件里放所有环境变量的“安全占位符”比如数据库URL、API密钥的key名但值全部留空或填。你的仓库里可以没有.env但绝不能没有.env.example——它是新成员上手的指南针也是代码可配置性的承诺书。我见过太多项目配置散落在各处有的在settings.py里有的在docker-compose.yml里有的直接写在os.environ.get(SOMETHING)但没有任何说明。目录结构解决不了所有问题但至少给你一个“所有配置应该有归属”的框架。演进路径小项目不必按满配走但骨架要长对我不希望你误解成“所有项目都要建这么多目录”。一个只有200行代码的脚本确实不需要src和services。但我的建议是哪怕你只写一个能用一小时的小工具也要把“包”和“入口”的边界划出来。你可以从最小结构起步minitool/ ├── src/ │ └── minitool/ │ ├── __init__.py │ ├── __main__.py │ └── core.py ├── tests/ │ └── test_core.py ├── pyproject.toml └── README.md当增长到来时你先加services/再加models/再拆scripts和configs。这种演进是顺滑的因为“包”的概念从一开始就是对的。反之如果一开始就把代码平铺在根目录后期迁移的成本远大于前期多敲三行路径。目录结构是唯一一个“越早重构越便宜”的东西而大多数人恰恰把它放到了最后。我还有什么反惯例的建议聊聊反惯例的。很多人喜欢把tests放在src内部我反对——测试是外部观察者包不应该知道测试的存在。还有人喜欢在包内放resources/比如静态数据、模板文件我建议仅在包内放“运行时非读不可”的资源其他一律放到项目级别的assets/。包内的资源越多其可移植性和可安装性就越差。如果你用importlib.resources去访问包内文件那可以接受否则用一个cursor变量指向项目根目录比..两层出去的路径美观得多。另一个反惯例我通常不在包内放version.py而是把版本号写进pyproject.toml然后通过importlib.metadata.version(mypackage)在代码里获取。版本信息是元数据不是业务代码。这样你改版本只改一处不会出现__init__.py里一个版本号、打包配置里另一个版本号的尴尬。结构是活的但原则不变说了这么多,你可能会觉得“这不过是个人偏好”。但我想强调目录结构不是美学问题而是工程问题。它的目的不是让项目看起来整齐而是让每个文件都有一目了然的“职责地址”让每次import都有明确的路径可循让每个新加入的开发者都能在十分钟内判断出“这段逻辑应该放哪”。我在实际项目中反复体会到当目录结构清晰时重构是安全的测试是轻松的依赖是显式的。当目录结构混乱时哪怕代码写得再漂亮团队也会在“你帮我看下这个函数在哪个文件”的对话中消耗掉所有协作热情。所以下一次你从零开始一个新Python项目别急着敲第一行代码。先花十五分钟把那个承载代码的“骨架”立起来。你要做的不是建一堆空文件夹而是给未来的每一个逻辑决策安排好一个不会后悔的家。那十五分钟是整个项目生命周期里回报率最高的一笔投资。