别再乱放文件了,Python项目结构这么搭最省心
项目中建新文件的时候我经常想一个问题这个文件放哪工具类放 utils还是 helpers还是 common还是 lib每个项目都有自己的叫法。换了项目又要重新猜。去年重构了一个老项目把这个问题彻底想了一遍。记录一下结论。先说项目长什么样一个 Python 项目最怕的就是平铺——所有文件堆在一个目录里几百个 .py 谁也找不到谁。我现在的标准布局是这样project/├── src/│ └── project/│ ├── __init__.py│ ├── main.py│ ├── config.py│ ├── models/│ ├── services/│ ├── repositories/│ └── utils/├── tests/│ ├── conftest.py│ └── test_services/├── scripts/├── docs/└── pyproject.toml说几个关键点。src 目录以前我喜欢把代码直接放根目录后来发现几个问题第一导入的时候容易搞混。项目里的 test.py 和 Python 标准库的 test 重名了跑的时候导入的是自己那个同事 review 的时候看半天才发现。第二打包的时候 src 目录能把项目代码和配置文件分开。setup.py、pyproject.toml、README 这些在根目录代码在 src 里不会混在一起。第三测试好写。用 src 布局安装项目的时候 pip install -e . 就能在测试里 import project而不是靠 PYTHONPATH 硬凑。config 单独放一个文件config.py 里放什么我的规矩是只放配置读取逻辑不放业务代码。python# config.pyimport osfrom dataclasses import dataclass, fielddataclassclass DatabaseConfig:host: str localhostport: int 5432name: str appdataclassclass RedisConfig:host: str localhostport: int 6379dataclassclass AppConfig:env: str developmentdebug: bool Truedb: DatabaseConfig field(default_factoryDatabaseConfig)redis: RedisConfig field(default_factoryRedisConfig)def load_config() - AppConfig:cfg AppConfig()cfg.env os.getenv(APP_ENV, development)cfg.debug cfg.env developmentcfg.db.host os.getenv(DB_HOST, localhost)cfg.db.port int(os.getenv(DB_PORT, 5432))return cfg不把 os.getenv 散落在各个文件里所有配置来源集中在一个文件改配置字段的时候只改一个地方。models、services、repositories 三兄弟这是我从 DDD 那里抄来的简化版。不搞那么复杂核心就三个概念models 存数据结构不存逻辑。dataclass 或者 Pydantic只定义字段和校验。python# models/user.pyfrom dataclasses import dataclass, fieldfrom datetime import datetimedataclassclass User:id: intname: stremail: strcreated_at: datetime field(default_factorydatetime.now)repositories 存数据操作不存业务。怎么从数据库拿数据、怎么存进去放在这里。python# repositories/user_repo.pyfrom models.user import Userclass UserRepository:def __init__(self, db_session):self._session db_sessiondef get_by_id(self, user_id: int) - User | None:row self._session.execute(SELECT * FROM users WHERE id ?, (user_id,)).fetchone()if row:return User(**row)return Nonedef save(self, user: User) - None:self._session.execute(INSERT INTO users (id, name, email, created_at) VALUES (?, ?, ?, ?),(user.id, user.name, user.email, user.created_at),)self._session.commit()services 存业务逻辑不关心数据来源。比如用户注册、发邮件、算积分。python# services/user_service.pyfrom models.user import Userfrom repositories.user_repo import UserRepositoryclass UserService:def __init__(self, repo: UserRepository):self._repo repodef register(self, name: str, email: str) - User:user User(idNone, namename, emailemail)existing self._repo.get_by_email(email)if existing:raise ValueError(f邮箱 {email} 已被注册)self._repo.save(user)return user这样分的好处是改数据库从 MySQL 换到 PostgreSQL只改 repositoriesservices 完全不用动。改注册逻辑只改 services管你数据放哪。tests 怎么组织测试目录结构和 src 对应方便找。tests/├── conftest.py├── test_services/│ └── test_user_service.py└── test_repositories/└── test_user_repo.py每个测试文件对应一个源文件test_user_service.py 测 services/user_service.py不混在一起。conftest.py 里放公共 fixture别每个文件都写一遍。python# tests/conftest.pyimport pytestfrom repositories.user_repo import UserRepositoryfrom services.user_service import UserServicepytest.fixturedef user_repo():return UserRepository(db_session:memory:)pytest.fixturedef user_service(user_repo):return UserService(repouser_repo)踩过的几个坑1. utils 目录不应该存在。或者只应该放纯函数——日期格式化、字符串处理这种。把业务逻辑塞进 utils 是个坏习惯因为它会变成垃圾场什么东西都能往里扔。我见过一个项目 utils.py 一万行。2. services 和 repositories 不要互相引用。service 依赖 reporepo 不要依赖 service。否则改一个地方可能影响一串。3. 测试不要依赖实体的 __init__ 方法里自动生成的东西。比如 created_at 用 datetime.now()测的时候时间不一致断言失败。改成显式传参测试里自己控制。4. 别过早拆分。项目只有三个文件的时候不用搞 models/services/repositories。等 services 目录里有了三个文件再拆。先让代码跑起来再让它变整齐。5. 大项目可以考虑加一个 interfaces 目录放抽象类这样 services 依赖抽象而不是具体实现。小项目不用直接依赖就行以后需要再提取接口。用了半年这个结构之前一个项目几个月没碰回来打开代码目录扫一眼就知道去哪改——用户相关去 services/user_service.py数据操作去 repositories/user_repo.py配置去 config.py。不用回忆不用翻整个目录。新同事上手也快。之前带人先花半小时讲项目结构。现在说一句models 存数据services 存逻辑repos 存数据库操作基本懂了。项目结构说白了就是个放文件的地方但放对了后面省很多事。