🛢️ 第23章 Python+MySQL实现在线学习笔记
📄 一句话概括

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 秒实验

  1. 把第②段的 n 从 10 改成 50,看单次耗时是变稳还是变化。
  2. 换成 import json, re, os, sqlite3 再跑一次,看导入多个库的边际成本。
  3. 把第③段的 :memory: 换成文件库,看「连接不要钱」这个结论还成不成立。

常见误区

  1. 把 venv/ 提交到仓库。仓库会大几百兆,而且换台机器就废——虚拟环境里是绝对路径。 写进 .gitignore。
  2. requirements.txt 不写版本号。半年后装出来的是另一个版本的库,代码可能跑不起来。 交付时这种问题集中爆发。
  3. 建库不加 utf8mb4。MySQL 的 utf8 只有三个字节,存不了 emoji 和部分生僻字。 而且它不报错,只把中文变成问号。
自测三问(点开对答案) 1. 为什么 Python 项目按「层」分目录,PHP 项目按「页面」分? —— 因为 Python 项目只有一个入口 `app.py`,请求由路由分派,「页面」这个单位消失了; PHP 是一个页面一个文件,页面天然就是组织单位。 2. 「解释器启动是固定成本」这句话有什么用? —— 它说明「少 import 一点」在启动阶段没用,真正的解法是「不要每次重启进程」——这就是常驻进程、 进程管理器的意义。 3. 为什么 MySQL 建库一定要写 `utf8mb4`? —— 因为 `utf8` 在 MySQL 里只有三个字节,存不了四字节字符(emoji、部分生僻字)。 而且它不报错,只把字符变成问号。

目录和依赖说清了,下一篇讲技术准备:PyMySQL 的六个方法,和一套表单校验。

相关笔记