恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Python项目结构最佳实践:从脚本到可维护工程的完整指南

  • 首页
  • 资讯中心
  • /
  • Python项目结构最佳实践:从脚本到可维护工程的完整指南

相关资讯

51单片机入门 2026/9/8 16:32:09
NLP实战指南:从词向量到BERT,让机器真正读懂文本 2026/9/8 16:32:09
GPT-6 来了!OpenAI 新旗舰 Astra 深度解读:它到底能干什么? 2026/9/8 16:32:09

最新资讯

反欺诈里那些抓不到的团伙,交给图神经网络:PyG GraphSAGE 交易反欺诈实战
WinForm控件命名规范:一套拿来即用的前缀规则与落地指南
AlphaFrequency:实验性展示字体设计与展示全记录
Ghost E2E 测试工作区协作规范:AGENTS.md 中的工作流、校验闭环与 Playwright MCP 定位器发现
uTools插件机制:用一套搜索框替代几十个小工具
GitHub CLI 的 gh skill 命令实战:Agent Skills 的搜索、预览、安装、更新与发布

今日推荐

Redis缓存与离线预计算在大数据处理中的实战应用
Android 12热启动闪屏排查:从冷热启动差异到官方SplashScreen避坑指南
加密资产价值投资:原理、方法与实战策略

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Python项目结构最佳实践:从脚本到可维护工程的完整指南

发布时间:2026/9/8 16:37:09
Python项目结构最佳实践:从脚本到可维护工程的完整指南 先回答很多刚接触 Python 的人都会问的一个问题真有必要花时间琢磨“Python项目结构”这件事吗直接在一个.py文件里从上写到下跑通不就行了我的回答是如果你只写十行脚本自己用当然可以怎么舒服怎么来。但当你开始写一个会被复用、被测试、被别人甚至“三个月后的自己”维护的项目时文件随便乱放带来的代价会立刻显现——导入失败、路径找不到、功能不敢改、测试跑不起来。几乎每个从脚本过渡到项目的开发者都会在这上面狠狠踩几脚。所以我想用这篇东西把Python项目结构从设计思路到落地实操完整走一遍结合我这些年折腾爬虫、量化回测、模型复现这类实际项目的经验讲清楚什么样的结构适合什么场景、每一步为什么这么放、以及最常见的那几个坑到底怎么避开。无论你是刚学会print(hello)的新手还是想让团队协作更顺畅的开发者这篇文章都能当作一份能直接抄作业的参考。1. 为什么项目结构能决定你的开发效率1.1 项目结构本质是在管理“复杂度”我见过很多初学者以为“项目结构”就是把文件放到不同的文件夹里让目录看起来整齐。这个理解没错但只看到了表面。项目结构真正解决的问题是代码之间的“依赖关系”和“变动风险”。举个例子一个量化交易策略的代码如果你把所有东西都堆在一个main.py里最开始可能只有 200 行你觉得还好。但当你加了数据下载、指标计算、回测引擎、交易信号、参数优化之后文件涨到 2000 行这时候你想改“止损条件”就必须在 2000 行里反复滚动找人眼改完之后还要担心是不是影响了别的地方。如果你一开始就把数据获取、策略逻辑、回测框架、可视化拆成独立模块每个模块只负责一件事那么改“止损逻辑”时你只需要打开策略模块其他部分完全不用动。结构化的本质是让你把“复杂度”拆散到不同的小盒子里每个盒子内部可以复杂但盒子之间的接口尽量简单。Python 的模块和包机制天生就是干这个的。很多人写 Python 一上来就from xxx import *并且文件之间互相乱引导致项目依赖图完全是一团乱麻这种代码基本没有维护性可言。一个健康的项目结构应该让人从目录树就能大致猜出这个项目有什么功能从哪里开始跑核心代码在哪测试在哪。读代码的人不需要深入每个文件就能对项目有一个整体认识。1.2 结构混乱的典型症状与后果什么样的代码算“结构有问题”我总结了几个高频症状你可以对照一下自己的项目import语句报错尤其换一台机器或换了一个目录就报错说明有人直接用“当前运行目录”来导入而不是基于项目根目录。一个文件几千行函数之间靠全局变量传递数据改一处就崩三处。没有独立的测试目录想跑测试得先把整个业务逻辑跑一遍。配置参数散落在各个文件里数据库地址、API Key、阈值全写死在代码里这不仅是结构问题还是安全问题。想写单元测试却导不进来被测模块因为项目根目录没进sys.path。这些症状的根因几乎都不是“你写代码的水平不行”而是“一开始没有想好代码放在哪、怎么互相调用”。尤其是 Python 这种靠路径和模块名解析导入的语言目录结构直接决定了你import是否顺畅。这跟 Java 强制包名和目录一致还不一样Python 太灵活了灵活到你如果不自己定规则项目就会用自己的“混乱”来惩罚你。2. Python 项目的常见结构模式与选型思路2.1 从单脚本到包Python 项目的规模分级没有一种结构是“放之四海而皆准”的因为项目结构必须匹配项目规模和场景。我通常把 Python 项目分为四个量级不同量级对应不同的组织方式。第一级是纯脚本级场景是数据处理、文件整理、爬个小网站这类“跑一次就完事”的代码。这种项目通常只有一个或两三个.py文件不需要什么复杂目录但至少应该保证一个文件只做一类事。第二级是小项目级功能开始多起来有配置、有依赖、有多个模块互相调用比如带界面的小工具、一个小型 Web 服务、一个自动化脚本集合。这时候就需要引入包结构、配置管理和依赖清单了。第三级是标准应用级比如一个完整的 Web 后端、一个机器学习训练项目、一个量化回测系统。这种项目要跑得长久必须有 src 布局、测试目录、文档目录、配置管理、命令行入口封装。第四级是多包/工作区级比如多个服务共享一套基础库或者一个代码仓库里有算法、API、调度等多个独立可部署的组件。这种情况可能要上 monorepo 结构或者多个独立仓库配合发布工具来管理。我强烈建议新手不要一上来就套用第四级的复杂结构否则你会被空目录和组织规则劝退。但也不要用第一级的方式去写第三级的项目否则后面重构的代价比一开始认真设计大得多。2.2 不同项目规模的目录结构参考直接给三个我常用的模板大家可以根据自己项目的情况对号入座。先看最入门的小项目结构适合几百行到一两千行的小工具my_tool/ ├── main.py ├── config.py ├── utils.py ├── requirements.txt └── README.md这种结构适合“一个主入口两三个辅助模块”的场景。main.py负责入口和流程编排utils.py放通用函数config.py集中放配置。也许你在真实场景中需要更多文件但核心思路是主流程、工具函数、配置分离就够了。再看我比较推荐的标准项目结构适合 Web 应用、数据处理、机器学习项目这类正经要长期维护的my_project/ ├── src/ │ └── my_project/ │ ├── __init__.py │ ├── cli.py │ ├── config.py │ ├── models/ │ ├── services/ │ ├── utils/ │ └── ... ├── tests/ │ ├── __init__.py │ ├── test_config.py │ └── test_services.py ├── docs/ ├── scripts/ ├── pyproject.toml ├── README.md └── .gitignore这是典型的 src 布局。接下来我用大量篇幅展开讲这种结构里每个目录的作用和设计逻辑因为这正是很多 Python 项目最欠缺的部分。2.3 两种主流结构对比扁平布局与 src 布局我上面说了 src 布局那另一种很流行的是“扁平布局”也就是项目根目录直接放包文件夹my_project/ ├── my_project/ │ ├── __init__.py │ └── ... ├── tests/ └── pyproject.toml两种方式有什么区别扁平布局在早期很常见因为简单直接你把my_project文件夹放在项目根目录然后从项目根目录运行import my_project能直接找到。但问题是如果你在项目根目录下的测试文件、脚本、配置里也存在同名模块运行pytest或某个脚本时Python 会把项目根目录当作第一顺位的搜索路径这样导入的可能是“当前目录下的代码”而不是你通过 pip 安装的那个版本。src 布局的核心价值在于把真正的包代码全部归置到src/下一层项目根目录下不再直接暴露可导入的顶层包。这样你运行测试时代码不能“碰巧”被根目录路径导入而是必须通过安装比如pip install -e .才能在环境中可见。这保证了测试时导进来的代码和用户实际安装的代码一致避免了“我本地跑得好好的一打包就崩”这种魔幻问题。如果你是第一次接触 src 布局可能会觉得多套一层文件夹麻烦。但事实是这种结构调整带来的收益非常确定。我后来所有正式项目都改成 src 布局了最大的感受是测试更加可信打包意外变少并且在项目根目录放scripts/、tests/这类非包代码时再也不会和包代码互相夹缠。3. 核心目录与文件的职责拆解3.1 包目录 src/my_project代码唯一主场在 src 布局下你的核心业务代码全部放在src/my_project/里。这个目录就是“包”所有模块的导入都是从它开始的。比如from my_project.services import DataLoaderPython 会去src/my_project/services.py或src/my_project/services/里找。包内的进一步组织也很有讲究。我建议按“业务领域”切分子包而不是按“技术类型”切。举个例子一个爬虫项目里你可能有下载器、解析器、存储模块那么你可以建crawler/downloader.py、crawler/parser.py、crawler/storage.py。但如果按技术类型切建一个utils/把所有零碎函数丢进去很快就会变成一个无法维护的大杂烩。你想想当你需要找一个“去重并写入数据库”的函数时你是去crawler/storage.py里找更快还是去utils/third_party.py里翻更快显然是前者。所以我的建议是子包的划分要跟着功能走每一个子包有一个清晰的职责描述。当一个模块文件超过三四百行并且改动的理由不止一个时就是时候把它拆成子包了。__init__.py是包的初始化文件。在 Python 3 里它甚至可以是空的单纯用来标记目录是一个包。但更好的用法是在__init__.py中声明包的对外公共 API。比如你的包里有downloader.py、parser.py如果它们是内部实现那更合适的对外暴露方式是# src/my_project/__init__.py from .crawler import BaseCrawler from .config import load_config __all__ [BaseCrawler, load_config]这样用户只需要from my_project import BaseCrawler而不需要关心 BaseCrawler 是不是在my_project.crawler子模块里定义的。这给了你重构内部结构的自由度因为外部调用方式不变内部想怎么挪都行。3.2 测试目录 tests保证重构安全感的底线很多人写了多年 Python 都没有建 tests 目录的习惯觉得“写测试太麻烦”“小项目不需要测试”。但我见过太多人因为懒于写测试最终被自己写的代码坑得死去活来。Python 是动态语言没有编译器帮你查类型错误函数内部逻辑只要稍微绕一点不改测试直接改代码你根本不知道哪里会被踩雷。一个基础的测试目录长这样tests/ ├── conftest.py ├── test_config.py ├── test_services.py └── fixtures/conftest.py是 pytest 的插件和共享 fixture 的定义点。你可以在里面写一些通用的测试夹具比如创建临时数据库、准备测试用配置、mock 外部 HTTP 请求等。fixtures/放测试用的静态资源比如样例 CSV、图片、JSON 文件。写测试这件事不用追求一步到位。先把最核心的业务逻辑覆盖住比如配置解析、数据处理的关键函数、对外 API 的返回格式。至少保证你跑pytest的时候项目的核心功能不会在重构中悄悄挂掉。关于测试导入的问题我在后面问题排查章节会仔细讲。这里先剧透一个原则用 pytest 配合 src 布局然后在项目根目录用pip install -e .安装项目测试里统一from my_project.xxx import yyy避免写一堆改sys.path的“良方”。3.3 配置与入口管理pyproject.toml 取代散装的 setup.py如果你到现在还在用requirements.txt管依赖、用setup.py描述包那我建议你尽快迁移到pyproject.toml。这不是追新而是pyproject.toml已经是 Python 打包与项目元数据的统一标准几乎所有现代工具链都对它有良好的支持。一个最小可用的pyproject.toml长这样[build-system] requires [setuptools68] build-backend setuptools.build_meta [project] name my_project version 0.1.0 description 一个用于演示项目结构的小项目 requires-python 3.10 dependencies [ requests2.31, pydantic2.5, ] [project.optional-dependencies] dev [ pytest7, ruff0.1, mypy1.7, ] [project.scripts] my-project my_project.cli:main [tool.setuptools.packages.find] where [src]这里有几个值得注意的点。[tool.setuptools.packages.find]里where [src]告诉打包工具去src目录下找包这样pip install -e .装的就是 src 布局里的代码。[project.scripts]用于定义命令行入口安装后你在终端直接敲my-project就能执行my_project.cli模块里的main()函数。[project.optional-dependencies]里的dev依赖可以这样安装pip install -e .[dev]用这种方式管理项目比把依赖全写在requirements.txt里更规范因为你可以区分生产依赖和开发依赖还能更精确地描述 Python 版本要求。如果你对配置管理有兴趣可以把config.py里的常量集中到这个文件里也可以单独建一个config/目录或使用环境变量。但不要把敏感信息如密钥、数据库密码写进pyproject.toml那是配置文件和环境变量该管的事后面我细说。3.4 辅助目录scripts、docs、data 与 .gitignore项目里除了核心包和测试还会有一些辅助目录。scripts/放开发期的一次性脚本比如初始化数据库、批量导入数据、部署辅助脚本。这些脚本不会被安装到包里只是开发过程中用来解放双手的。docs/放项目文档最简单的就是README.md直接放根目录复杂项目可以做docs/下用 MkDocs 或 Sphinx 管理。我建议先把根目录的README.md写明白内容包括项目是什么、怎么安装、怎么运行、怎么测试、目录结构说明。这个文件是你项目的“门面”也是接手者最先看的东西。data/在数据处理和算法项目里尤其常见。原始数据放data/raw/处理后的中间数据放data/processed/最终产出放data/output/。但如果数据文件很大通常不会把数据提交进 Git而是在.gitignore里忽略掉整个data/目录再写一个数据下载或生成的脚本。.gitignore的重要性极高。很多人刚开始用 Git 时不小心把虚拟环境目录、__pycache__、.env文件、本地数据库等都提交到了仓库里轻则仓库臃肿重则泄露密钥。不管项目多小一开始就把.gitignore建好是最省心的做法。一个基础的 Python.gitignore至少应该包括__pycache__/ *.py[cod] .venv/ venv/ dist/ build/ *.egg-info/ .env .pytest_cache/ .mypy_cache/ .ruff_cache/如果你用 VS Code可能还要忽略.vscode/用 Jupyter 的话可以考虑.ipynb_checkpoints/。每个项目的实际忽略内容可以微调但上面这些基础项是通用的。4. 实操记录从空目录到一个规范 Python 项目4.1 手动搭建最小骨架不依赖脚手架工具市面上有很多项目脚手架工具比如cookiecutter可以用来一键生成项目结构。但如果你不理解生成出来的每一层目录是干什么的直接用脚手架反而会让你变成“只会套模板的人”。所以我建议先手动搭一次理解每层结构的意义之后再决定要不要用工具加速。手动搭建项目骨架最简单的路径是直接用文件系统命令创建目录和文件。我现在以创建一个名为demo_project的项目为例完整走一遍。第一步创建顶层目录结构mkdir demo_project cd demo_project mkdir src mkdir tests mkdir docs mkdir scripts第二步创建包目录和__init__.pymkdir src/demo_project touch src/demo_project/__init__.py第三步创建核心模块。这里我给出一个带业务逻辑的service.py一个管理配置的config.py一个作为入口的cli.py# src/demo_project/config.py from pathlib import Path from typing import Any import yaml class Config: def __init__(self, data: dict[str, Any]) - None: self.data data classmethod def from_yaml(cls, path: Path) - Config: with Path(path).open(r, encodingutf-8) as f: raw yaml.safe_load(f) return cls(raw or {}) def get(self, key: str, default: Any None) - Any: return self.data.get(key, default)# src/demo_project/service.py from pathlib import Path from demo_project.config import Config class ReportGenerator: 一个简单示例从配置读输入输出路径并生成文本报告。 def __init__(self, config: Config) - None: self.config config def run(self) - Path: input_path Path(self.config.get(input_path, input.txt)) output_path Path(self.config.get(output_path, output.txt)) content input_path.read_text(encodingutf-8) output_path.write_text( fReport generated based on: {content}\n, encodingutf-8 ) return output_path# src/demo_project/cli.py import argparse from pathlib import Path from demo_project.config import Config from demo_project.service import ReportGenerator def main() - None: parser argparse.ArgumentParser(descriptiondemo project) parser.add_argument(--config, typePath, defaultPath(config.yaml)) args parser.parse_args() config Config.from_yaml(args.config) generator ReportGenerator(config) output_path generator.run() print(fReport saved to {output_path}) if __name__ __main__: main()第四步创建测试文件。注意测试里导入的是demo_project包这也是为什么我们需要先安装项目再跑测试否则 pytest 找不到模块# tests/test_service.py from pathlib import Path from demo_project.config import Config from demo_project.service import ReportGenerator def test_report_generator(tmp_path: Path) - None: input_file tmp_path / input.txt input_file.write_text(hello world, encodingutf-8) config Config( { input_path: str(input_file), output_path: str(tmp_path / output.txt), } ) generator ReportGenerator(config) output_path generator.run() assert output_path.exists() assert hello world in output_path.read_text(encodingutf-8)第五步创建pyproject.toml这是让包可安装的核心。配好之后执行pip install -e .[dev]这里解释一下为什么用-e。-e表示 editable 安装也就是“开发模式”。它会创建一个指向你当前源码目录的链接之后你改代码不需要重新安装就能生效。如果你是第一次接触这个特性就足够让你在开发阶段离不开它了。安装完成后你在任意目录下运行python -c from demo_project.service import ReportGenerator; print(import ok)都能成功。这就解决了“换个目录代码就导入失败”的问题。第六步初始化 Git 并写好.gitignore。这一步看起来跟项目结构无关但其实是在保护你的结构不被乱七八糟的生成物入侵。4.2 用现代工具链管理依赖与环境我经常被人问虚拟环境到底用venv还是conda包管理用pip还是poetry还是pdm我给的答案比较务实如果你不需要处理复杂的非 Python 原生依赖比如某个 C 扩展库直接用venv加pip就完全够用。如果你需要做数据科学或者机器学习而依赖里经常有 numpy、pytorch 这类二进制包那么用 conda 或 micromamba 管理环境会更省心。为了项目结构的一致性我通常会做这样几件事在项目根目录创建.venv作为虚拟环境目录然后用.gitignore忽略它。写pyproject.toml并在其中维护依赖而不是把依赖散放在多个requirements.txt。用pip install -e .[dev]一次性安装开发依赖。用pre-commit钩子工具在提交前自动跑 ruff、black、mypy 等检查。这些工具体系看似和“目录长什么样”无关但它们决定了你的代码是否能被人轻松捡起来跑。举个例子如果项目没有锁定依赖版本半年后别人 clone 下来跑pip install -r requirements.txt发现某个库的大版本已经变了接口全改这个项目的复现性就为零。所以项目结构不仅是“文件在哪”还包括“依赖怎么描述、环境怎么重建”。4.3 不同类型的典型 Python 项目实践对比根据我的经验不同类型项目的结构细节会有一些差异。比如爬虫项目通常不是一个大包而是一个采集任务框架它可能长这样crawler_project/ ├── src/crawler_project/ │ ├── spiders/ # 每个站点一个爬虫 │ ├── pipelines/ # 数据处理流程 │ ├── middlewares/ # 下载中间件 │ ├── settings.py # 爬虫配置 │ └── run.py ├── tests/ ├── pyproject.toml └── scrapy.cfg再比如算法训练类项目由于它天然是“实验探索型”的代码不像 Web 服务那样有强入口通常会额外出现configs/目录放各种实验配置experiments/目录记录每次实验的输出和指标src下还会拆出data/、models/、trainers/等模块。我用跑过的一个 YOLO 类目标检测项目举例。很多人直接拿官方仓库的代码跑跑通了但对项目结构完全没有概念想改成自己的数据时根本不知道从哪下手。这类开源项目常见的组织方式是这样的yolo_project/ ├── configs/ │ ├── model.yaml │ ├── data.yaml │ └── train.yaml ├── src/yolo_project/ │ ├── data/ │ │ ├── dataset.py │ │ └── transforms.py │ ├── models/ │ │ ├── backbone.py │ │ └── head.py │ ├── trainer.py │ └── utils/ ├── scripts/ │ ├── download_data.py │ └── visualize_result.py ├── tests/ ├── requirements.txt └── README.md这种结构最大的好处是训练代码和配置完全分离。想调参数时不用翻代码改常量直接改configs/train.yaml想换模型结构时在models/里加一个新文件并在配置里指定名称而不是在训练循环里写满 if-else。所以你在设计自己的项目时不要硬抄某个结构而应该先问自己这个项目最常被改动的点是什么把最容易变的逻辑独立出来项目结构就成功了大半。另一个典型场景是量化交易策略代码。这种项目往往需要在回测与实盘之间快速切换。回测是研究逻辑实盘是稳定执行如果把这两类代码混在一个包里风险很高。我通常建议按“研究环境”和“运行环境”分隔quant_project/ ├── src/quant_project/ │ ├── datafeed/ # 数据源相关 │ ├── strategy/ # 策略信号逻辑核心、且纯函数化 │ ├── backtest/ # 回测引擎 │ ├── execution/ # 实盘执行与券商接口 │ ├── portfolio/ # 组合与风控 │ └── config.py ├── scripts/ │ ├── run_backtest.py │ └── run_live.py ├── tests/ ├── configs/ └── pyproject.toml策略模块应该设计成“不依赖任何交易接口的纯逻辑模块”输入行情和持仓输出目标仓位。这样回测和实盘共用一套策略代码不会因为实盘接口差异导致“回测一个样、实盘一个样”。这种模块划分的好坏就完全通过项目结构体现出来了。5. 项目实际运行中的配置与路径管理5.1 为什么你的代码总在路径上出问题运行项目时最经典的报错就是ModuleNotFoundError: No module named xxx或者FileNotFoundError: [Errno 2] No such file or directory。这两个问题看着不同根因往往都指向同一个代码没有基于“项目根目录”来定位模块和资源文件。很多初学者在main.py里写相对路径比如with open(data/input.txt, r) as f: ...这种写法依赖“程序当前所在的目录”也就是说你必须cd到项目根目录再执行python -m my_project.cli文件才能被找到。如果你在项目根目录之外用 IDE 直接运行main.py或者用系统的定时任务从别的路径唤起脚本这个相对路径就失效了。正确做法是用绝对路径或基于文件位置的定位。一个很好的工具是pathlibfrom pathlib import Path # 当前文件所在目录 BASE_DIR Path(__file__).resolve().parent # 项目根目录src/my_project/config.py 的上一级是 src再上一级是根目录 PROJECT_ROOT BASE_DIR.parent.parent DATA_DIR PROJECT_ROOT / data把任何路径都基于Path(__file__).resolve()而不是基于当前工作目录项目能在任何地方被调用。特别要注意不要在代码里手工拼字符串路径比如os.path.join(os.getcwd(), data)因为getcwd()表示的是“当前进程的工作目录”它会随启动方式而变化。5.2 建议用轻量配置而不是写死常量项目结构里还有一个容易忽略的点配置。如果项目只有一两个配置项你直接在config.py里写常量就够了API_BASE_URL https://api.example.com DEFAULT_TIMEOUT 30但当配置项变多特别是不同环境开发、测试、生产下取值不一样时写在代码里就是灾难。常见做法是用 YAML、TOML 或环境变量来管理配置。我在小项目中常用的做法是建一个configs/目录里面有config.yaml、config.test.yaml等文件然后在代码里写一个加载函数。这里关键是“默认配置路径”不要写死让用户可以通过命令行参数或环境变量指定# src/my_project/config.py import os from pathlib import Path import yaml DEFAULT_CONFIG_PATH Path(configs/config.yaml) def load_config(path: str | None None) - dict: config_path Path(path or os.getenv(APP_CONFIG, DEFAULT_CONFIG_PATH)) with config_path.open(r, encodingutf-8) as f: return yaml.safe_load(f)在 CLI 入口里再暴露--config参数这样无论从哪里启动使用的人都清楚知道“这个项目的配置入口在哪”。如果涉及密钥和敏感信息一定不要提交到 Git 仓库。你应该把它们放到环境变量里或者放到本地.env文件配合python-dotenv使用并且把.env写进.gitignore。这一步既是安全要求也是结构管理的一部分因为敏感配置一旦进了代码库无论之后怎么删都会留在 Git 历史里。6. Python 项目结构避坑指南与常见排查技巧6.1 最容易被项目结构坑到的瞬间做项目结构这件事你很少会因为结构“正确”而获得即时反馈但一定会因为结构“错误”而付出代价。我把踩过的坑按出现频率排个序第一个坑是把自己写的项目当成“环境变量里的可执行文件”来跑结果各种找不到模块。很多新手写完一个项目直接在 IDE 里右键运行src/my_project/cli.py结果from my_project.config import Config报错。因为此时 Python 把“src/my_project”目录加入搜索路径而my_project这个包名需要从“src”目录开始才能导入。解决办法有两个最省事的在 IDE 里把工作目录设置为项目根目录并且用模块方式运行python -m my_project.cli但更彻底的办法是利用我们前面说的 editable install先在项目根目录执行pip install -e .安装以后 Python 环境中就有了my_project这个包无论在哪个目录下启动 Python都能import my_project。自此之后你就不用靠“运气”来跑代码了。第二个坑是项目结构定了但谁都记不住该在哪放代码。这就不只是技术问题了需要团队约定或 README 里写明结构。我一般会在 README 里放一个“代码组织”小章节几句话说清楚每个目录放什么。如果没人看 README那就在__init__.py或模块 docstring 里写清楚职责。第三个坑是 Python 缓存和旧字节码导致的“改了 code 没生效”错觉。当你重构项目时如果移动过模块老的__pycache__可能还在Python 有时会命中旧的缓存文件。虽然正常情况下 Python 会根据源文件时间戳判断缓存是否过期但你手动移动文件夹时确实可能遇到诡异情况。遇到此类问题优先清理所有__pycache__find . -type d -name __pycache__ -exec rm -rf {} 再重新运行往往就能恢复。6.2 循环导入与相对导入问题处理在包内部互相导入时另一个高频地雷是循环导入。比如module_a.py里from .module_b import func_b而module_b.py又from .module_a import func_a。当程序真正执行到导入的瞬间两个模块都还没创建完于是 Python 抛出ImportError: cannot import name ...。避免循环导入的方法不是“调整 import 顺序”而是从结构上消除循环依赖。常见策略有三种把公共函数下沉到一个新的模块比如common.py让两个模块都只依赖common而不再互相依赖。把其中一个依赖改为延迟导入在函数体内导入而不是在模块顶层导入。把类或函数作为参数传入而不是在模块内部直接引用对方。第三种方式在设计上更优雅但也需要调用方做出配合。我在实际项目中遵循一个原则顶层模块之间尽量不互相导入依赖关系应该形成有向无环图而不是一个环。如果发现某个模块既需要 A 又需要 B而 A 又反向依赖了它说明职责划分有问题需要继续拆分。还有一类问题是相对导入和绝对导入混用。在包内部from .module import something是相对导入.module是相对于当前模块所在的包。在包内部使用相对导入可以避免顶层包重名时出现的混乱。但如果在入口文件cli.py里直接用相对导入然后你把cli.py当作脚本直接执行python cli.pyPython 会报ImportError: attempted relative import with no known parent package。因为脚本运行时Python 不认为它属于任何包。所以我的习惯是包内部各个模块之间用相对导入包外入口只用绝对导入。比如cli.py入口文件用from my_project.config import Config而config.py内部想导入同包的utils则用from .utils import helper。6.3 常见问题速查表一次解决“跑不起来”的尴尬我把我这些年经常遇到的、和项目结构强相关的报错信息整理成一个速查表大家可以当做一个 checklist报错场景常见原因推荐解法No module named my_project包未安装或当前目录不在搜索路径中在项目根目录执行pip install -e .然后用模块方式运行运行时FileNotFoundError代码中使用了相对路径依赖当前工作目录改用Path(__file__).resolve()定位项目根目录拼接绝对路径从脚本直接运行报相对导入错误脚本被当作顶层模块运行不知道父包是谁入口脚本使用绝对导入或用python -m pkg.xxx方式执行改代码后运行结果不变可能命中旧的__pycache__缓存清理__pycache__目录后重试安装时报packages没找到setuptools 不知道包位置在pyproject.toml配置[tool.setuptools.packages.find] where [src]测试里 import 不到被测模块测试目录没有安装项目安装开发模式pip install -e .[dev]测试内部统一用绝对导入执行不同目录的脚本导入策略不一致脚本期望的工作目录不同把所有脚本改成基于项目根目录定位并提供统一--config入口可能大多数人在刚开始看这张表的时候还会觉得有些条目抽象。不要紧你只需要记住一条核心思路让项目根目录成为所有路径、导入、配置的相对基准并用“安装”而不是“运气”来让 Python 认识你的包。把你写的代码当成一个需要被安装、被调用的正经包你的很多路径和导入问题会同时消失。6.4 别照抄别人的结构先考虑可测试性与扩展点网上能看到很多开源项目的目录结构比如教育类项目、爬虫框架、算法库等。你可以参考但千万不要照抄。每个项目的“变与不变”不一样抄来的结构可能不适合你反而成为负担。举个例子如果你想开发的是一个类库被其他人使用目录结构里src布局、类型注解、文档生成就是重点。如果你开发的是给老板看的报表生成脚本那么一个干净的入口加可视化输出可能比规范的包结构更重要。如果你做的是一套算法实验框架那配置和实验追踪的目录设计可能比代码本身还要关键。我的建议是在动手写核心代码前先花半小时画一画模块视图。不用画很精细的 UML 图就写清楚从哪个入口开始执行它会依赖哪些模块每个模块依赖哪些数据数据从哪里来结果输出到哪里。画完这张图你的目录结构大概就出来了。同时注意可测试性。如果某个模块被设计成导入时立刻联网、立刻读数据库那这个模块就很难被测试。更合理的做法是将“副作用”操作放到cli.py的main()里核心模块保持输入输出简单这样测试只需构造输入然后断言输出。这其实不是项目结构的问题而是代码组织意识的问题但项目结构能大幅放大或限制这种意识。你问一个 Python 写了很多年的同学他最庆幸的事是什么往往不是某天写出了一个多么漂亮的算法而是项目从一开始就被组织得恰到好处——当他需要给代码加测试、加新功能、换数据源时都不用对整个项目动大手术。7. 这类项目结构的扩展方向从单包到多包与工作区7.1 什么时候需要把代码拆成多个包我前文讲的都是“单包项目”的组织方式。随着代码量上涨你会发现一个包内塞的东西越来越多。当出现以下信号时你可能需要考虑拆包src/my_project/下面光一级子目录就有十几个且互相依赖很弱。项目的一部分将来要单独复用比如一个数据清洗库既被当前项目用也被未来新项目用。项目包含多个可独立部署的组件比如一个 API 服务加一个定时任务处理器它们除了共享模型和工具函数之外没什么关联。不同部分的依赖差异很大比如一个组件依赖pandas另一个组件只依赖fastapi。如果全放一起用户安装了 API 服务就不得不背上数据科学的重量级依赖。这种情况下仓库内可以演化出多个包结构。如果只是同一个仓库里既有算法库又有 Web 接口可以考虑保留单仓库但把两个组件分别放到src/algo_lib/和src/api_service/两个顶层包中中间通过一个共享包进行通信。如果你管理的是更大规模的代码库比如一个组织里有多个服务共享公共常量和数据库模型可以考虑把公共代码单独建一个包。但拆包不是越细越好包的粒度太碎会导致版本发布和依赖管理变得很麻烦。拆包前先问一下自己这个模块真的会被其他项目直接 import 吗还是说只是当前位置不太好看而已如果只是目录不好看那就留在当前包里继续重构目录组织而不是急着拆出去。7.2 多包工作区的管理策略在我个人的实践里如果有一个仓库需要同时开发多个包我会特别关注两个工具editable install 和统一测试入口。假设仓库结构是workspace/ ├── src/ │ ├── shared_lib/ │ ├── service_a/ │ └── service_b/ ├── tests/ │ ├── test_shared_lib/ │ ├── test_service_a/ │ └── test_service_b/ ├── pyproject.toml这时候我通常不建一个“包住一切”的 pyproject而是在每个包目录下单独维护 pyproject.toml根目录的 pyproject 只放统一工具链配置比如 ruff、pytest、mypy 的公共配置。开发时先把 shared_lib 以 editable 方式安装到虚拟环境再安装 service_a 和 service_b这样所有包都在同一个虚拟环境中可用测试也能在根目录统一跑。如果包之间必须独立发布可以给每个包单独维护版本号。这是一个很大的话题但基础的目录组织逻辑不会变仓库被拆成多个独立包每个包有自己的核心代码、测试和打包配置共享的开发和发布规范尽量写在根配置中避免每个包都维护一份重复内容。这些都是从单包结构自然延伸的。先把当前项目做好别在一开始就设计一个宇宙级工程。8. 最后再分享一点个人体会写了这么多年 Python我自己最深的感受是项目结构不是“能不能跑起来”的问题而是“你还能不能继续改下去”的问题。一个结构混乱的项目前期代码涨得飞快越到后期越发寸步难行一个结构清晰的项目前期可能要多花几十分钟搭目录、写 pyproject、建测试目录但后期每一次改动的成本都被压得很低。所以如果你现在正被自己的代码坑得头疼不妨停下来花一个下午把项目结构重新梳理一遍。你不一定需要一步到位用 src 布局也不一定马上把所有配置迁到 pyproject.toml你只需要遵守三条简单原则第一核心代码和入口脚本分离第二配置文件不写死在代码里路径不依赖当前工作目录第三从第一天就建立 tests 目录和 .gitignore。这三件事做好你的项目就已经超过相当一部分同行了。接下来再慢慢迭代向着更规范的结构演进回头你会发现这半天时间花得实在太值了。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号