尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用Python做项目时,我是这样组织代码结构的

用Python做项目时,我是这样组织代码结构的 项目结构不是用来应付人的是用来救命的。刚开始用Python写项目时我也曾把一堆脚本扔在同一个文件夹里靠文件名区分功能三个月后再打开自己都不敢改。后来经过多次重构和被线上故障毒打我摸索出一套适合中小型Python项目的组织方式核心原则只有一条让代码的意图在目录结构中就能被看见而不是靠注释和回忆。先分清楚“入口”和“逻辑”我的项目根目录永远只有几样东西一个包目录比如myapp/、一个tests/目录、一个pyproject.toml、一个README.md以及偶尔出现的scripts/目录。很多初学者喜欢把main.py放在根目录这没有错但我会更进一步——入口文件只负责启动不允许写业务逻辑。根目录下的main.py通常只有两三行from myapp.main import run if __name__ __main__: run()真正的东西全部在包内。包内第一层就是模块边界我会按照“基础设施—领域逻辑—应用服务—接口层”四层来切分。myapp/下面至少有config.py、models/、services/、repositories/、api/、core/其中core/放配置读取、日志初始化、数据库连接等横切关注点。如果一个文件既能干数据库查询又能算业务金额那这个文件就是一个定时炸弹迟早会在某个深夜爆掉。用配置模块代替散落的魔法常量我见过太多项目把数据库链接、超时时间、重试次数直接硬写在业务函数里改一个参数要全局搜索。我的做法是让config.py成为唯一的配置入口它从环境变量和.env文件中读取值并提供类型安全的访问接口。from pydantic_settings import BaseSettings class Settings(BaseSettings): app_name: str MyApp database_url: str sqlite:///./app.db debug: bool False max_retries: int 3任何模块都不应该直接导入os.environ而是from myapp.config import settings。当“配置”成为一个对象而不是一堆裸字符串时你会发现自己再也不怕改需求了。配置和业务代码的隔离是代码结构的第一层免疫力。模块划分要符合“稳定依赖方向”我遵循一条铁律依赖只能从外层指向内层绝不允许反向依赖。具体来说api/层依赖services/层services/层依赖repositories/层repositories/层只依赖models/和core/。这样划分后每个模块都能独立测试因为它的依赖都是更稳定的模块。举个例子一个用户注册流程api/routes.py负责解析HTTP请求把参数交给服务层services/user_service.py处理业务规则比如检查邮箱是否重复repositories/user_repo.py只做数据操作models/user.py定义数据结构和ORM映射。如果某天要换数据库只改repositories和core/db.py业务层和接口层完全不受影响。这种分层带来的最大好处不是“整洁”而是把变化的范围压缩到最小——你不需要重写半个项目就能切换技术栈。用包来隐藏内部实现而不是用下划线很多人喜欢用_utils.py、_helpers.py来放公共函数这其实是一种偷懒。模块名一旦叫“utils”就会变成垃圾场。我倾向于把工具函数按领域归位比如services/validators.py、services/formatters.py、core/time_utils.py。如果某个函数确实很通用而且被多个业务模块使用我会把它提升到core/下而不是让业务模块直接互相调用。命名一个包就是声明一种边界如果边界模糊你就等着代码里去数不清的import xxx_utils吧。另外包的__init__.py不只是凑数文件。我会在__init__.py中重新导出该包对外的主要接口这样外部调用时from myapp.services import create_user而不是from myapp.services.user_service import create_user。这能让依赖关系更清晰也方便未来内部重构而不影响调用方。数据模型和业务对象要分开许多框架尤其是Django容易让人把数据库模型当成业务对象直接在模型上写方法、加属性、存临时状态。我在项目里坚持将数据持久层模型和领域模型分离即使麻烦一点也要分开。models/目录下放ORM模型只负责字段定义和基础关系services/层使用数据模型但会组装成简单的dataclass或TypedDict作为返回对象。这样操作的好处是测试时不需要数据库也能构造业务对象而且避免模型类越来越臃肿。虽然会增加一些转换代码但长期来看每一次在模型上添加与数据库无关的方法都是在给ORM挖坑也是在给未来的自己增加负重。异常处理要建立自己的异常层次我自己的项目里业务代码禁止直接抛出ValueError或裸的Exception因为这样会让调用方无法区分“参数不对”和“余额不足”。我会在core/exceptions.py中定义基础的AppError然后派生出NotFoundError、ValidationError、ConflictError等。class AppError(Exception): pass class NotFoundError(AppError): pass class ValidationError(AppError): pass在服务层业务方法只抛出这些自定义异常在接口层统一捕获AppError并转为对应的HTTP状态码或错误响应。没有自定义异常体系的项目错误处理迟早会变成一团乱麻因为每个模块都在按自己的方式表达失败。日志是项目结构的灵魂我组织代码时会把日志设置放在core/logging_config.py但不会让每个模块自己写logging.basicConfig。模块中使用logger logging.getLogger(__name__)这样日志天然带有模块路径信息排查问题时一眼就能定位。日志级别和输出格式是全局配置而不是模块内部的事。我还会在core/logging_config.py中定义过滤器比如屏蔽健康检查的请求日志避免噪音。有一句话我深有体会项目结构混乱时日志是最后的安全网项目结构清晰时日志只是辅助工具。但无论如何日志必须在第一天就设计好否则重构时你会对着一堆没有上下文的信息发呆。依赖注入用最简单的方式构造函数对于中小型项目我不会引入复杂的依赖注入框架而是使用朴素的构造函数注入。比如UserService需要仓库和缓存就显式在__init__里接收class UserService: def __init__(self, user_repo: UserRepo, cache: RedisCache): self._repo user_repo self._cache cache然后在api/routes.py中一次性组装依赖树。这样做虽然有点样板化但优点突出每个对象的依赖一目了然测试时可以轻松替换成Mock。显式依赖比隐式全局变量好一万倍哪怕多写几行代码也值得。使用全局单例对象在早期很爽但项目变大后你会陷入“这个状态是谁改的”的深渊。测试目录镜像包结构我的tests/目录和myapp/目录保持完全对应的结构。tests/test_services/test_user_service.py对应myapp/services/user_service.py。这样写测试时不用思考“该放哪里”而且跑测试时也能快速覆盖新增代码。测试代码也是代码它应当被同样严格地组织。对于每个服务类我会写两个测试文件一个是正常的单元测试用Mock替换数据库另一个是集成测试连接真实测试库。集成测试放到tests/integration/下单元测试直接镜像模块路径。此外conftest.py中只放fixture不放测试逻辑。这只是一种习惯但能让整个测试套件看起来清爽且容易定位失败。使用pyproject.toml统一管理工具链项目根目录的pyproject.toml是我最重视的文件之一。它不仅声明依赖还管理运行工具ruff用来做代码检查pytest配置测试路径和覆盖范围mypy开启严格模式。让工具配置跟着项目走而不是靠每个人的编辑器设置。这样团队协作时不管谁拉下来代码都能跑出一致的风格检查结果。我在[tool.mypy]中设置strict true虽然初期会触发很多类型报错但坚持下来后代码的作者、调用者和维护者都能快速理解数据流动。类型提示不是给解释器看的是给未来的你和其他开发者看的地图。小心循环导入通过模块层次解决循环导入是Python项目结构混乱最常见的症状。根源往往是模块职责不清比如A需要用到B的类型而B又需要用到A的函数。我的解决策略是把共享的类型或常量放到更底层的模块中或者将其中一个依赖延迟到方法内部导入。但更根本的做法是如果发现两个模块互相依赖立刻把它们依赖的那部分拆分到第三模块比如core/types.py。另外对于from myapp.x import y的导入我倾向于在每个模块顶部集中导入不写函数内导入除非是在处理真正的循环引用场景。循环导入往往是一次重构的警报——提示你模块之间的层级已经乱了。资源文件和数据文件单独存放如果项目需要读取模板、SQL脚本或静态资源我不会把它们放在Python包内而是放在assets/或resources/目录并在core/paths.py中定义路径解析函数。这样避免打包时把资源文件混进代码包也方便部署时挂载数据卷。代码和数据分离是项目管理中容易被忽视却非常关键的一环尤其当项目需要作为Docker镜像发布时。不要忽视scripts/目录我保留一个scripts/目录专门存放管理命令行工具的脚本比如初始化数据库、生成测试数据、数据迁移辅助脚本。这些脚本通常只做一件事调用包内的核心函数。我不会允许这些脚本写复杂的SQL或业务逻辑否则它们会成为测试盲区也会变成新的“垃圾堆”。脚本的目的是让人机交互变简单而不是把业务逻辑再复制一遍。面对实际项目的柔性调整上面这套结构不是万能的。如果你的项目是数据管道可能不需要api/层但你会需要pipelines/和datasources/如果是机器学习项目则可能有features/和models/但核心的分层思想仍然适用稳定的底层不依赖易变的上层。我会在每个项目初始化时花二十分钟画一个简单的依赖图然后问自己哪个模块应该最稳定哪个模块最容易变化把这些答案反映到目录结构里比写十页设计文档都管用。组织代码结构其实是在管理复杂度的氧化过程。每天合并代码都会引入新的连接如果不刻意维护边界这些连接会迅速生长成藤蔓把整个项目绑成一团。好的结构不是一蹴而就的而是每次修改代码时都要问一句“这个新依赖方向对吗这个模块是不是塞了太多职责”这样的习惯才是我能持续在Python项目中保持清醒的真正秘诀。
返回列表