代码语言

知识点思维导图

28 个知识节点

Python(30) - 项目结构与规范

读完后,你应能完成以下任务:

  • 绘制“Python(30) - 项目结构与规范 / 先建立直觉:Python 项目 ≈ 前端项目的目录组织”的关键对象与数据流,解释“边界(这里和前端不一样):前端的「依赖隔离」是自动的——每个项目自带 node_modules,你几乎不用操心。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(30) - 项目结构与规范 / 目录组织:一个标准 Python 项目长什么样”设计正常与异常输入,验证“新手最容易把所有 .py 文件平铺在根目录,跑是能跑,但项目一大就乱。”,输出首个偏差位置与回归测试结果。
  • 实现“Python(30) - 项目结构与规范 / 配置管理:别把配置写死在代码里”的最小代码或配置,检验“边界:pyproject.toml 用的是 TOML 格式,不是 JSON——别习惯性写大括号和逗号。”,输出命令、结果与 Diff,并说明不适用边界。

前端项目你闭着眼都能搭:src/ 放代码、package.json 管依赖、.env 放配置、出问题翻控制台。Python 这套东西全都有,但叫法和习惯不一样——目录怎么分层、配置往哪放、密钥怎么不进 git、日志为什么不能用 print。这篇把「一个像样的 Python 项目长什么样」讲清楚,让你写出来的代码别人接手不骂街。

一、先建立直觉:Python 项目 ≈ 前端项目的目录组织

类比:你脑子里前端项目的样子——根目录一堆配置文件(package.json.env.gitignore),源码全塞 src/,依赖装进 node_modules/——这套心智模型几乎可以原样搬到 Python。每一块都有对应物:

前端 Python 作用
src/ src/包名/包名/ 源码主目录
package.json pyproject.toml 项目元信息 + 依赖声明
node_modules/ venv/(虚拟环境) 第三方依赖装这里
.env .env 环境变量 / 密钥
.gitignore .gitignore 忽略不进库的文件
index.js(入口) main.py / __main__.py 程序入口
console.log logging 模块 日志输出
tests/ tests/ 测试代码(见第 31 篇)
README.md README.md 项目说明

边界(这里和前端不一样):前端的「依赖隔离」是自动的——每个项目自带 node_modules,你几乎不用操心。Python 默认所有项目共用一套全局解释器,不手动建虚拟环境就会版本打架(详见第 12 篇)。所以 Python 项目的「规范」第一条永远是:进项目先激活 venv。这点心智负担是前端没有的。


二、目录组织:一个标准 Python 项目长什么样

新手最容易把所有 .py 文件平铺在根目录,跑是能跑,但项目一大就乱。社区有约定俗成的结构。先看一个典型的中小项目(以 FastAPI 后端为例):

myproject/
├── pyproject.toml          # 项目元信息 + 依赖(≈ package.json)
├── README.md               # 项目说明
├── .gitignore              # git 忽略清单
├── .env                    # 本地配置/密钥(绝不进 git)
├── .env.example            # 配置模板(进 git,标明需要哪些变量,但不填真值)
│
├── src/                    # 源码根(src layout,下面解释为什么用它)
│   └── myproject/          # 真正的包,包名 = 项目名
│       ├── __init__.py     # 让 myproject 成为一个包(≈ index 入口)
│       ├── main.py         # 程序入口
│       ├── config.py       # 配置集中管理
│       ├── api/            # 路由层(子包)
│       │   ├── __init__.py
│       │   └── users.py
│       ├── services/       # 业务逻辑层
│       │   └── __init__.py
│       ├── models/         # 数据模型
│       │   └── __init__.py
│       └── utils/          # 工具函数
│           └── __init__.py
│
├── tests/                  # 测试(和 src 平级,见第 31 篇)
│   └── test_users.py
│
└── logs/                   # 日志输出目录(一般 gitignore 掉)

这套分层(api / services / models)和你在后端见过的 MVC 三层是一个思路:路由只管收发请求,业务逻辑收在 services,数据结构放 models

2.1 src layout vs flat layout

你会看到两种摆法,区别只在「包要不要再套一层 src/」:

# flat layout(扁平)—— 包直接放根目录
myproject/
├── pyproject.toml
└── myproject/          # 包和配置文件同级
    └── __init__.py

# src layout(推荐)—— 包放进 src/
myproject/
├── pyproject.toml
└── src/
    └── myproject/      # 包多套一层 src
        └── __init__.py

为什么推荐 src layout(讲 WHY 而非 WHAT):flat 布局下,你在根目录运行测试时,Python 会因为「当前目录在 sys.path 里」(详见第 8 篇)而直接 import 到源码目录,哪怕你根本没安装这个包。这会掩盖「忘了声明某个依赖」之类的打包问题——本地跑得好好的,装到别人机器上就崩。src layout 强制你把包真正安装一遍pip install -e .,类似前端的 npm link)才能 import,测的就是「用户实际拿到的东西」,把问题暴露在自己机器上。

边界:小脚本、一次性项目用 flat 完全没问题,别上来就搞 src 把自己绕晕。要发布成库、或者项目会长期维护,再上 src layout。


三、配置管理:别把配置写死在代码里

前端你早就知道「配置和代码分离」:API 地址、密钥放 .env,用 import.meta.envprocess.env 读。Python 完全一样的思路,工具不同而已。

3.1 项目元信息:pyproject.toml(≈ package.json)

现代 Python 项目的「中央配置文件」,声明项目名、版本、依赖,连各种工具(格式化、测试)的配置都能塞进去:

# pyproject.toml —— 角色等于前端的 package.json
[project]
name = "myproject"              # 项目名
version = "0.1.0"               # 版本号(≈ package.json 的 version)
requires-python = ">=3.10"      # 要求的 Python 版本(≈ engines.node)
dependencies = [                # 运行时依赖(≈ dependencies)
    "fastapi>=0.110",
    "pydantic-settings>=2.0",
    "python-dotenv>=1.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff"]   # 开发依赖(≈ devDependencies)

边界pyproject.toml 用的是 TOML 格式,不是 JSON——别习惯性写大括号和逗号。它和老式的 requirements.txt 不冲突:requirements.txt 是「锁死的扁平清单」(≈ lockfile 的简化版),pyproject.toml 是「带元信息的依赖声明」(≈ package.json 本体)。详见第 12 篇。

3.2 环境变量与密钥:.env + python-dotenv

和前端一模一样:敏感信息(数据库密码、API key)放 .env.env.gitignore,只把不含真值的 .env.example 提交

# .env —— 本地真实配置,绝不进 git
DATABASE_URL=postgresql://user:secret@localhost/mydb
OPENAI_API_KEY=sk-真实密钥
DEBUG=true

读取方式有两种。最朴素的是 python-dotenv + os.getenv,几乎是前端 dotenv 的翻版:

并排看前端,思路完全一致:

// 前端 / node
import 'dotenv/config'                 // 加载 .env 到 process.env
const dbUrl = process.env.DATABASE_URL ?? 'sqlite:///./local.db'

3.3 推荐做法:用 pydantic-settings 做「类型安全的配置」

os.getenv 的毛病你在前端也踩过:取出来全是字符串,"true" 不是布尔,少配一个变量要运行到那行才报错。Python 的 pydantic-settings 能把配置变成一个带类型校验的配置对象(类似你用 zod 校验过的 config),启动时就把类型转好、缺失项报出来。

为什么比裸 os.getenv:配置项集中在一个类里一目了然;类型自动转换("8000"8000"true"True);必填项缺失会在启动瞬间报错,而不是跑到用它的那行才崩——这正是你用 TS/zod 想要的「尽早暴露问题」。

边界:字段名默认不区分大小写地匹配环境变量(database_url 字段匹配 DATABASE_URL),这点和你手写 os.getenv("DATABASE_URL") 大小写敏感不同,别被绕到。


四、日志:为什么不能用 print

新手最大的坏习惯:调试全靠 print。前端你早就从 console.log 进化到分级日志(console.warn / console.error,或者 pino / winston)了,Python 也有标准的 logging 模块,生产代码一律用它,别用 print

4.1 print 和 logging 的差别(讲 WHY)

print 的问题不是「不能用」,而是:不能分级别(没法只看错误、屏蔽调试信息)、没有时间戳/来源、不能统一改输出去向(控制台 / 文件 / 远程)、上线后想关掉得逐个删。logging 把这些全解决了——这和你不会在生产代码里留一堆 console.log 是同一个道理。

console(前端) Python logging 级别含义
console.debug logger.debug() 最啰嗦的调试细节
console.log / info logger.info() 正常流程信息
console.warn logger.warning() 警告,没崩但要注意
console.error logger.error() 出错了
(无) logger.critical() 致命错误

4.2 基本用法

输出长这样(自带时间、来源、级别,print 给不了):

2026-06-11 10:30:00,123 [myproject.api.users] INFO: 开始创建用户: imber
2026-06-11 10:30:00,124 [myproject.api.users] INFO: 用户创建成功: imber

4.3 写到文件 / 滚动日志

生产环境日志要落盘,且要防止单文件无限变大。用 logging.handlers

边界 1(高频踩坑)logging.basicConfig 只在根日志器尚未配置时生效一次,第二次调用默认被忽略。很多人「日志配置没生效」就是因为别处(或某个库)已经先配过了。要么集中在程序入口配一次,要么用 force=True 强制覆盖。

边界 2:日志参数用 logger.info("用户 %s 登录", name) 这种占位符 + 参数的写法,别用 f-string(f"用户 {name} 登录")。WHY:占位符写法只有在该级别真要输出时才做字符串拼接,DEBUG 级别被过滤时省掉拼接开销;这是 logging 的设计约定,和 print 直接传字符串不同。


五、入口文件:让项目「能被跑起来」

前端 package.json"main": "index.js"scripts 指定入口。Python 常见两种:

# 对应两种运行方式
python src/myproject/main.py      # 直接跑脚本
python -m myproject               # 以模块方式跑包(推荐,包路径解析更可靠,详见第 8 篇)

六、.gitignore:哪些东西绝不能进 git

前端你知道不提交 node_modules.env。Python 要忽略的东西类似,但多了些 Python 特有的:

# 虚拟环境(≈ node_modules,体积大、可重建,绝不进库)
venv/
.venv/

# Python 编译缓存(运行时自动生成的字节码,无需提交)
__pycache__/
*.pyc

# 配置与密钥(≈ 前端的 .env,含敏感信息)
.env

# 日志、本地数据库
logs/
*.log
*.sqlite3

# IDE / 测试缓存
.idea/
.pytest_cache/

边界__pycache__/.pyc 是 Python 运行时自动生成的字节码缓存(CPython 把 .py 编译成字节码缓存下来加速下次启动),前端没有完全对应物——别看到陌生就去提交它,加进 .gitignore 忽略即可。


七、常见踩坑清单

  1. 所有 .py 平铺在根目录:小脚本无所谓,项目一大必乱。按 api / services / models / utils 分层,每个目录记得放 __init__.py(详见第 8 篇)。
  2. 密钥硬编码进代码、甚至提交进 git:API key、数据库密码一律走 .env.env 必须 .gitignore,只提交 .env.example 模板。已经误提交的密钥要当作泄露处理(改掉它)。
  3. 生产代码用 print 当日志:换成 logging,分级别、带时间来源、可统一改去向。
  4. logging.basicConfig 配了没生效:它只生效一次。集中在入口配,或用 force=True
  5. 配置散落各处 os.getenv:集中到一个 config.pySettings 类(pydantic-settings),类型安全、缺失早报错。
  6. src layout 下 import 不到自己的包:src 布局需要先 pip install -e . 把包装成可编辑模式(≈ npm link)才能 import,这是它的特性不是 bug。

八、总结

  • 先建立直觉:Python 项目 ≈ 前端项目的目录组织:| console.log | logging 模块 | 日志输出 |
  • 目录组织:一个标准 Python 项目长什么样:新手最容易把所有 .py 文件平铺在根目录,跑是能跑,但项目一大就乱。
  • 配置管理:别把配置写死在代码里:边界:pyproject.toml 用的是 TOML 格式,不是 JSON——别习惯性写大括号和逗号。
  • 日志:为什么不能用 print:print 的问题不是「不能用」,而是:不能分级别(没法只看错误、屏蔽调试信息)、没有时间戳/来源、不能统一改输出去向(控制台 / 文件 / 远程)、上线后想关掉得逐个删。
  • 入口文件:让项目「能被跑起来」:前端 package.json 里 "main": "index.js" 或 scripts 指定入口。
  • .gitignore:哪些东西绝不能进 git:边界:pycache/ 和 .pyc 是 Python 运行时自动生成的字节码缓存(CPython 把 .py 编译成字节码缓存下来加速下次启动),前端没有完全对应物——别看到陌生就去提交它,加进 .gitignore 忽略即可。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“项目结构与规范”中,需要同时满足“先建立直觉:Python 项目 ≈ 前端项目的目录组织”与“目录组织:一个标准 Python 项目长什么样”。给定正文约束“你脑子里前端项目的样子——根目录一堆配置文件(package.json、.env、.gitignore),源码全塞 src/,依赖装进 nodemodules/——这套心智模型几乎可以原样搬到 Python。”,哪些判断保持了原有处理机制?多选
2“项目结构与规范”出现偏差:“在“项目结构与规范 / src layout vs flat layout”中,即使不满足“你会看到两种摆法,区别只在「包要不要再套一层 src/」”,结果与副作用仍会保持不变。”已成为实际行为。围绕“src layout vs flat layout”与“配置管理:别把配置写死在代码里”,哪些判断能定位被改变的职责或边界?多选
3评审“项目结构与规范”方案时,验收条件包含“现代 Python 项目的「中央配置文件」,声明项目名、版本、依赖,连各种工具(格式化、测试)的配置都能塞进去。”。关于“项目元信息:pyproject.toml(≈ package.json)”与“环境变量与密钥:.env + python-dotenv”的哪些决策符合正文机制?多选