Python 项目按「层」分目录:models 管数据、forms 管校验、templates 管视图。
原因是它只有一个入口 app.py,页面这个概念被路由取代了。
解释器启动是一笔固定成本:和代码好坏无关,只和「是否每次重启进程」有关。
教材 23.3 讲系统开发必备:开发工具准备、文件夹组织结构、项目使用说明。 这一节在三章项目里最像「说明书」,但它藏着一个 Python 特有的性能事实。 PHP 每个请求启动一个进程,Python 常规做法是一个常驻进程处理所有请求。 这个差别决定了「连接池」「全局对象」这两个概念在两种语言里的难度完全不同。 演示里实测了「启动一个 Python 进程要多久」——这台机器上要半秒,普通机器几十毫秒。 数字大小不重要,重要的是它是固定的。
一、目录:按层分,不按页面分
| 目录 | 放什么 | 对应哪一层 |
|---|---|---|
app.py |
入口、路由 | 控制层 |
config.py |
数据库地址、密钥 | 配置 |
db.py |
数据库操作类 | 数据访问层 |
forms.py |
表单与校验 | 输入层 |
models/ |
表对应的函数 | 领域层 |
templates/ |
HTML 模板 | 视图层 |
PHP 项目常见的是按页面分:list.php、edit.php、del.php,每个文件自成一体。
Python 项目不这么分,因为它只有一个入口——所有请求都从 app.py 进,由路由分派到函数。
「页面」这个单位消失了,剩下的组织维度就只有「职责」。
这两种分法没有对错,但它们会塑造代码的样子。
按页面分,容易写出「一个文件里既有 SQL 又有 HTML」;
按层分,SQL 天然会集中到 db.py 或 models/ 里。
这正好对上 23.1 那条规矩「SQL 只在一个类里写」——目录结构本身就是规矩的一部分。
二、依赖:三行 requirements 就够
教材这个项目只依赖三个包:Flask(Web 框架)、PyMySQL(连库)、WTForms(表单校验)。
它们的安装方式是一行 pip install,但工程上还要多做一件事:把依赖写进文件。
| 文件 | 作用 |
|---|---|
requirements.txt |
记版本号,别人装得和你一样 |
.gitignore |
别把 venv/ 和 *.db 提交上去 |
第一行的重点是「版本号」。只写 Flask 而不写版本,今天装和半年后装可能差三个大版本,
而三个大版本之间的改动足以让代码跑不起来。
这类问题在交付、换机器、上服务器的时候集中爆发。
第二行是新手最常犯的:把虚拟环境目录提交到仓库,仓库一下大几百兆。
而且它没用——虚拟环境里全是绝对路径,换台机器就废了。
三、解释器启动:一笔固定成本
这一节是 Python 和 PHP 最大的运行时差别。
| 语言 | 谁在跑请求 | 启动成本付几次 |
|---|---|---|
| PHP | 每个请求一个进程 | 每个请求都付 |
| Python(常规) | 一个常驻进程 | 启动时付一次 |
演示里量了「启动一个 Python 进程」的成本:这个沙箱里要半秒左右,普通机器在几十毫秒量级。 半秒这个数偏大,但它说明了一件事:启动是「固定成本」,和你的代码写得好不好无关。 这个差别带来两个后果:
| 后果 | 说明 |
|---|---|
| Python 要常驻 | 所以有 gunicorn、uWSGI 这类「进程管理器」 |
| 连接池才有意义 | 进程常驻,连接才能跨请求复用 |
第二行值得多说:PHP 里讨论「连接池」其实是在讨论「怎么让连接跨进程复用」, 这需要额外的中间件(比如 MySQL 的连接代理)。 而 Python 里连接池就是一个进程内的对象池,几行代码。 同一个词,两种难度——这完全是运行时模型决定的。
四、启动说明:三行命令也要写
教材的「项目使用说明」写了怎么建库、怎么改配置、怎么启动。 这三步看着琐碎,但它是「别人能不能跑起来」的唯一保障。
| 步骤 | 常见坑 |
|---|---|
| 建库导数据 | 字符集不是 utf8mb4,中文变问号 |
| 改配置文件 | 密码写死在代码里,换机器就报错 |
| 启动 | 端口被占,或者忘了开虚拟环境 |
第一行的坑最隐蔽:数据能进去,但中文全是问号,而且不报错。
原因是数据库或表的字符集不是 utf8mb4——注意「不是 utf8」,MySQL 的 utf8 只支持三个字节,
存不了 emoji 和部分生僻字。
所以建库语句里那个 CHARACTER SET utf8mb4 不是可选项。
第二行的坑在交付时爆发:开发机上密码是 123456,到了客户那里是别的,于是要改代码、
重新打包。
正确做法是配置从环境变量读,代码里只留一个「读配置」的函数。
五、动手 Demo:项目目录,和「每次请求都重新启动」的代价
这一段先看目录结构怎么按层分,再量两件事:启动一个 Python 进程要多久、多导入一个标准库要多花多少。 后一个数会说明「贵的是启动本身」。
import subprocess, sys, time
# ① 目录结构
for line in (u"note/",
u" ├─ app.py 入口,路由都在这",
u" ├─ config.py 数据库配置",
u" ├─ db.py 数据库操作类",
u" ├─ forms.py 表单与校验",
u" ├─ models/ 表对应的函数",
u" ├─ templates/ 模板(HTML)",
u" └─ static/ 样式与脚本"):
print(line)
# ② 启动一个进程要多久
exe = sys.executable
def boot(code, n=10):
t = time.perf_counter()
for _ in range(n):
subprocess.run([exe, u"-c", code], capture_output=True)
return (time.perf_counter() - t) / n * 1000
print(u"空启动 %.1f ms" % boot(u"pass"))
print(u"带 sqlite3 %.1f ms" % boot(u"import sqlite3"))
# ③ 对比:一个常驻进程里重复调用
import sqlite3
c = sqlite3.connect(u":memory:")
t = time.perf_counter()
for _ in range(10):
c.execute(u"SELECT 1").fetchone()
print(u"常驻进程里跑 10 次 %.3f ms" % ((time.perf_counter() - t) * 1000))
c.close()
实跑输出:
--------------------------------------------------------------------
项目目录,和「每次请求都重新启动」的代价
--------------------------------------------------------------------
**教材 23.3 是系统开发必备:开发工具准备、文件夹组织结构、项目使用说明。**
**这一节讲两件事:目录怎么分,以及 Python 项目一个特有的成本——解释器启动。**
① 目录按功能分,还是按类型分
--------------------------------------------------------------------
note/
├─ app.py 入口,路由都在这
├─ config.py 数据库配置
├─ db.py 数据库操作类
├─ forms.py 表单与校验
├─ models/ 表对应的函数
├─ templates/ 模板(HTML)
└─ static/ 样式与脚本
**Python 项目和 PHP 项目在这一点上思路不同:PHP 按「页面」分目录,**
**Python 更常见按「层」分——`models` 放数据访问、`forms` 放校验、`templates` 放视图。**
**原因是 Python 项目通常只有一个入口 `app.py`,所有请求都从它进,**
**所以「页面」这个概念被路由取代了,目录自然按职责分。**
② 解释器启动:Python 特有的固定成本
--------------------------------------------------------------------
空启动 529.1 ms
带 sqlite3 546.2 ms
**空启动就要 529 毫秒,这是「启动解释器 + 加载标准库」的固定成本。**
**这台机器上这个数偏大(跑一次子进程要半秒),普通机器一般在几十毫秒量级。**
**但量级差异不重要,重要的是它是【固定】的:和代码写得好不好完全无关,**
**只和「是否每次都重新启动进程」有关。**
**所以「用 Python 写 Web」和「用 PHP 写 Web」在这一点上有本质差别:**
**PHP 每次请求启动进程(成本在连接和进程上),**
**Python 的常规做法是「一个常驻进程处理所有请求」,启动成本只付一次。**
**另外注意第二行比第一行只多 17.1 毫秒——导入一个标准库几乎不花钱,**
**贵的是「启动进程」这件事本身。**
**这也解释了为什么 Python 项目更强调「连接池」「全局对象」——因为进程常驻,**
**这些东西才能跨请求复用。**
**而 PHP 里讨论连接池,讨论的其实是「怎么让连接跨进程复用」,难度完全不同。**
第二段两个数是关键:空启动和带 sqlite3 只差一点点。 也就是说,导入一个标准库几乎不花钱,贵的是「启动进程」这件事本身。 这也说明「少 import 一点能提速」这种优化,在启动阶段基本没用。 这台机器上启动要半秒,明显比普通机器慢(普通几十毫秒)。 但不管快慢,它都是【固定】成本——所以「常驻进程」这个设计的意义就出来了:付一次,用很久。 第三段的对比最能说明问题:同一个查询跑十次,在常驻进程里只要零点几毫秒。 因为进程没重启、连接没重建、字节码没重新加载。 这就是为什么 Python 项目里「连接池」是个自然的选择,而 PHP 里它需要一个额外的服务。
30 秒实验
- 把第②段的
n从 10 改成 50,看单次耗时是变稳还是变化。 - 换成
import json, re, os, sqlite3再跑一次,看导入多个库的边际成本。 - 把第③段的
:memory:换成文件库,看「连接不要钱」这个结论还成不成立。
常见误区
- 把
venv/提交到仓库。仓库会大几百兆,而且换台机器就废——虚拟环境里是绝对路径。 写进.gitignore。 requirements.txt不写版本号。半年后装出来的是另一个版本的库,代码可能跑不起来。 交付时这种问题集中爆发。- 建库不加
utf8mb4。MySQL 的utf8只有三个字节,存不了 emoji 和部分生僻字。 而且它不报错,只把中文变成问号。
自测三问(点开对答案)
1. 为什么 Python 项目按「层」分目录,PHP 项目按「页面」分? —— 因为 Python 项目只有一个入口 `app.py`,请求由路由分派,「页面」这个单位消失了; PHP 是一个页面一个文件,页面天然就是组织单位。 2. 「解释器启动是固定成本」这句话有什么用? —— 它说明「少 import 一点」在启动阶段没用,真正的解法是「不要每次重启进程」——这就是常驻进程、 进程管理器的意义。 3. 为什么 MySQL 建库一定要写 `utf8mb4`? —— 因为 `utf8` 在 MySQL 里只有三个字节,存不了四字节字符(emoji、部分生僻字)。 而且它不报错,只把字符变成问号。目录和依赖说清了,下一篇讲技术准备:PyMySQL 的六个方法,和一套表单校验。
- ☐ (本篇完,下一篇 23.4 技术准备)