首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用这套Python项目结构模板,代码交接再也不用口头解释

用这套Python项目结构模板,代码交接再也不用口头解释

作者头像
用户11081884
发布2026-07-20 20:36:02
发布2026-07-20 20:36:02
300
举报

设想如下场景:同事离职,走之前把他负责的项目“交接”给了我。

说是交接,其实就是在工位上指着屏幕跟我讲了40分钟:“这个config是配置,那个utils是工具函数,scripts下面有几个脚本但其中两个没用了别管它,tests……嗯测试我后来没怎么写……”

我当时就想,如果项目结构本身就能说话,何必费这口舌?

后来我接手第二个、第三个项目,发现每个项目目录长得都不一样。有的把所有代码扔一个文件夹,有的层层嵌套七八层,有的README写了一堆但关键信息一个没有。每次接手新项目,都得靠前任口头“翻译”。

痛定思痛,我整理了一套Python项目结构模板,自己用了大半年,团队里推了一阵子。不敢说是最佳实践,但至少交接的时候不用再口头解释了——打开项目,目录结构就是文档。


先看全貌

代码语言:javascript
复制
my-project/
├── README.md              # 项目入口,必读
├── pyproject.toml         # 依赖管理 + 构建配置
├── Makefile               # 常用命令快捷入口
├── .env.example           # 环境变量模板
├── .gitignore
├── .github/
│   └── workflows/
│       └── ci.yml         # CI配置
├── docs/
│   └── architecture.md    # 架构设计文档
├── scripts/
│   ├── setup.sh           # 一键初始化
│   └── seed_data.py       # 测试数据填充
├── tests/
│   ├── conftest.py        # 公共fixture
│   ├── unit/
│   │   └── test_service.py
│   └── integration/
│       └── test_api.py
├── src/
│   └── my_project/
│       ├── __init__.py
│       ├── __main__.py    # python -m my_project 入口
│       ├── config.py      # 配置加载
│       ├── models/        # 数据模型
│       ├── services/      # 业务逻辑
│       ├── api/           # 对外接口
│       └── utils/         # 工具函数
└── docker/
    ├── Dockerfile
    └── docker-compose.yml

别急着说“太复杂了”。我一个个讲,你看看这些文件是不是都有必要。


为什么用 src/ 布局?

这个可能是争议最大的点。很多人习惯了这样:

代码语言:javascript
复制
my-project/
├── my_project/
│   ├── __init__.py
│   └── ...
├── tests/
└── pyproject.toml

项目根目录直接放包,看起来简单。但有个坑:你本地开发的时候,Python会从项目根目录找模块,这意味着你跑的是源码而不是安装后的包。本地跑得好好的,一打包发布就挂——因为安装路径、依赖关系可能都不一样。

把包放在 src/ 下面:

代码语言:javascript
复制
src/
└── my_project/
    ├── __init__.py
    └── ...

必须先 pip install -e . 安装才能用,本地开发环境和真实安装环境保持一致。这个做法Python官方文档和PyPA都推荐,叫 src layout。

一开始我也觉得多一层目录麻烦,但踩过一次“本地能跑装不上”的坑之后,就再也没回过头。


pyproject.toml 而不是 requirements.txt

以前项目里总有这些东西:

代码语言:javascript
复制
requirements.txt
requirements-dev.txt
setup.py
setup.cfg
MANIFEST.in

哪个是干啥的?新来的人一脸懵。

pyproject.toml 一个文件搞定,PEP 518/621标准,Python社区已经定了:

代码语言:javascript
复制
[project]
name = "my-project"
version = "0.1.0"
description = "项目描述"
requires-python = ">=3.10"
dependencies = [
    "fastapi>=0.100.0",
    "sqlalchemy>=2.0",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-cov",
    "ruff",
    "mypy",
]

[project.scripts]
my-project = "my_project.__main__:main"

依赖、元数据、入口点,全在这一个文件里。不需要再维护 setup.pyrequirements.txt 两套东西。

开发环境安装:

代码语言:javascript
复制
pip install -e".[dev]"

一行搞定,开发和运行依赖都装上。


Makefile — 团队新人的救命稻草

新同事入职第一天, clone完代码问你:“怎么跑?怎么测?怎么格式化?”

以前我得口头说:先装依赖,然后跑pytest,flake8检查代码风格……现在我只说一句:make

代码语言:javascript
复制
.PHONY: install test lint format run clean

install:
pip install -e ".[dev]"

test:
pytest tests/ -v --cov=my_project

lint:
ruff check src/ tests/
mypy src/

format:
ruff format src/ tests/

run:
python -m my_project

clean:
rm -rf build/ dist/ *.egg-info .pytest_cache .mypy_cache
find . -type d -name __pycache__ -exec rm -rf {} +

make test 跑测试,make lint 检查代码,make run 启动项目。不用记命令,不用翻文档,make 一下就能看到所有可用命令。

Makefile的本质很简单:把团队约定变成可执行文件,而不是口头约定。


config.py — 别再把配置散落在各处

你有没有见过这种代码?

代码语言:javascript
复制
# service.py
DB_HOST = "192.168.1.100"   # 硬编码
API_KEY = "sk-xxxxx"        # 直接写源码里
TIMEOUT = int(os.environ.get("TIMEOUT", "30"))  # 到处都是环境变量读取

配置散落在十几个文件里,改一个数据库地址要翻遍整个项目。更别提密钥直接写在代码里这种操作——我见过不止一次推到Git上去了。

用一个统一的配置模块:

代码语言:javascript
复制
# src/my_project/config.py
frompydantic_settingsimportBaseSettings
fromfunctoolsimportlru_cache

classSettings(BaseSettings):
    # 应用配置
    app_name: str = "my-project"
    debug: bool = False

    # 数据库
    db_host: str = "localhost"
    db_port: int = 5432
    db_name: str = "mydb"
    db_user: str = ""
    db_password: str = ""

    # 外部服务
    api_key: str = ""
    api_timeout: int = 30

    model_config = {"env_file": ".env"}

@lru_cache
defget_settings() ->Settings:
    returnSettings()

所有配置集中管理,类型有校验,默认值有文档,环境变量自动读取。哪里需要配置就 from my_project.config import get_settings,不用到处 os.environ.get

.env.example 是给新人看的模板:

代码语言:javascript
复制
# .env.example — 复制为 .env 后填写实际值
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=your_user
DB_PASSWORD=your_password
API_KEY=your_api_key
API_TIMEOUT=30

新人clone完项目,复制 .env.example.env,填上自己的配置,就能跑起来。不用再问你“数据库地址是什么”“API密钥找谁要”。


models/services/api/ — 分层不是装腔作势

很多人写Python喜欢把所有逻辑塞一个文件,甚至一个函数从头写到尾。几百行的函数,数据库查询、业务判断、响应格式化全搅在一起。改一处怕牵连其他,最后谁都不敢动。

拆成三层:

代码语言:javascript
复制
src/my_project/
├── models/          # 数据长什么样
│   ├── __init__.py
│   ├── user.py      # 用户模型
│   └── order.py     # 订单模型
├── services/        # 业务怎么干
│   ├── __init__.py
│   ├── user_service.py
│   └── order_service.py
├── api/             # 外部怎么调
│   ├── __init__.py
│   └── routes.py

各层干的事不一样:

  • models/:纯数据定义,不管业务逻辑。用SQLAlchemy定义ORM,用Pydantic定义Schema,别混着来。
  • services/:核心业务逻辑。查数据库、调外部接口、做计算,都在这里。这是项目最核心的部分,也应该是最好测试的部分——因为不依赖HTTP框架,mock起来方便。
  • api/:HTTP接口层。只负责接请求、调service、返响应。业务逻辑一行都别写。

举个具体的例子:

代码语言:javascript
复制
# models/user.py — 数据模型
fromsqlalchemyimportColumn, String, Boolean
frommy_project.models.baseimportBase

classUser(Base):
    __tablename__ = "users"
    id = Column(String, primary_key=True)
    name = Column(String)
    email = Column(String, unique=True)
    is_active = Column(Boolean, default=True)
代码语言:javascript
复制
# services/user_service.py — 业务逻辑
frommy_project.models.userimportUser
frommy_project.configimportget_settings

classUserService:
    def__init__(self, db_session):
        self.db = db_session

    defget_user(self, user_id: str) ->User|None:
        returnself.db.query(User).filter_by(id=user_id).first()

    defdeactivate_user(self, user_id: str) ->bool:
        user = self.get_user(user_id)
        ifnotuser:
            returnFalse
        user.is_active = False
        self.db.commit()
        returnTrue
代码语言:javascript
复制
# api/routes.py — 接口层
fromfastapiimportAPIRouter, Depends
frommy_project.services.user_serviceimportUserService

router = APIRouter()

@router.get("/users/{user_id}")
defget_user(user_id: str, service: UserService = Depends()):
    user = service.get_user(user_id)
    ifnotuser:
        return {"status": "not_found"}
    return {"id": user.id, "name": user.name}

这样分层的好处:改接口不影响业务逻辑,换数据库不影响接口,写单元测试可以绕过HTTP直接测service。每一层只关心自己的事,改动的范围才可控。


__main__.py — 统一入口

之前见过各种启动方式:有人 python app.py,有人 python run.py,有人 python -m some.module.that.is.deep.nested。每个项目不一样,每次都得问。

__main__.py

代码语言:javascript
复制
# src/my_project/__main__.py
frommy_project.configimportget_settings
frommy_project.apiimportcreate_app

defmain():
    settings = get_settings()
    app = create_app(settings)
    app.run(host="0.0.0.0", port=8000)

if__name__ == "__main__":
    main()

启动方式统一:

代码语言:javascript
复制
python -m my_project

不用记入口文件叫啥,不用翻README找启动命令。python -m 包名,就这一种。


tests/ — 分清单元测试和集成测试

很多项目的 tests/ 目录就是一个大杂烩,单元测试和集成测试混在一起。跑起来有的快有的慢,只想跑快的还得分着挑。

分开来:

代码语言:javascript
复制
tests/
├── conftest.py          # 公共fixture
├── unit/                # 不依赖外部,秒级完成
│   └── test_user_service.py
└── integration/         # 需要数据库/外部服务
    └── test_api.py
代码语言:javascript
复制
# tests/conftest.py
importpytest
frommy_project.services.user_serviceimportUserService

@pytest.fixture
defuser_service():
    """单元测试用的mock service"""
    returnUserService(db_session=MockSession())

日常开发跑单元测试,速度快:

代码语言:javascript
复制
pytest tests/unit/ -v

CI或者上线前跑全量:

代码语言:javascript
复制
pytest tests/ -v

这比一锅炖的体验好太多了。


scripts/ — 那些只跑一次的脚本也得有个家

每个项目总有几个“偶尔用一下”的脚本:初始化数据库、填充测试数据、迁移旧数据、清理临时文件。以前这些脚本要么散落在项目根目录,要么在某个人的电脑上,要么压根找不到。

给它们一个固定位置:

代码语言:javascript
复制
scripts/
├── setup.sh          # 一键初始化开发环境
└── seed_data.py      # 填充测试数据

setup.sh 是新人入职的第一条命令:

代码语言:javascript
复制
#!/bin/bash
set-e

echo">>> 创建虚拟环境"
python -m venv .venv
source .venv/bin/activate

echo">>> 安装依赖"
pip install -e".[dev]"

echo">>> 配置环境变量"
cp .env.example .env

echo">>> 初始化数据库"
python -m my_project.db init

echo">>> 完成!运行 make run 启动项目"

这样不管谁来,clone完跑一个 bash scripts/setup.sh 就能开始干活。


docs/ — 不是让你写长篇大论

很多项目要么没文档,要么文档是过时的。问题出在“文档”这个词太吓人了,好像得写个几十页的规范似的。

docs/ 里就放一个文件:architecture.md

内容不超过一页,写三件事就够了:

  1. 这个项目是干啥的(一两句话)
  2. 目录结构每个部分是干啥的(就是上面那些解释)
  3. 关键决策和原因(比如为什么选FastAPI不选Flask,为什么用src布局)
代码语言:javascript
复制
# 项目架构

## 简介
内部订单管理系统,提供REST API给前端调用。

## 目录说明
- `src/my_project/models/` — 数据模型(ORM + Schema)
- `src/my_project/services/` — 业务逻辑
- `src/my_project/api/` — HTTP接口
- `tests/unit/` — 单元测试(不需要数据库)
- `tests/integration/` — 集成测试(需要数据库)

## 关键决策
- 使用FastAPI:需要异步支持 + 自动生成API文档
- src布局:避免本地开发和安装环境不一致
- Pydantic Settings:统一配置管理,类型安全

够用了。以后有人问“这个项目为什么这样组织”,指这个文件就行。


最后说个事

其实项目结构这件事,最怕的不是“不知道怎么组织”,而是“每个人都按自己的习惯来”。

模板的意义从来不是“这个结构最牛”,而是“大家用同一套”。当所有项目长得差不多,你打开任何一个都能在10秒内找到想要的东西,这才是效率。

代码能自己说话,就不用人替它解释。

“无他,惟手熟尔”!有需要的用起来!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nicholas与Pypi 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先看全貌
  • 为什么用 src/ 布局?
  • pyproject.toml 而不是 requirements.txt
  • Makefile — 团队新人的救命稻草
  • config.py — 别再把配置散落在各处
  • models/、services/、api/ — 分层不是装腔作势
  • __main__.py — 统一入口
  • tests/ — 分清单元测试和集成测试
  • scripts/ — 那些只跑一次的脚本也得有个家
  • docs/ — 不是让你写长篇大论
  • 最后说个事
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档