About Hello Python
[!IMPORTANT] 最好的学习方式是间隔性重复练习。
一个编程高手是怎样练成的?惟手熟尔,重在刻意练习。 这意味着不断学习、练习、实践、再实践,直到熟练掌握所有技能。
Hello,Python 是如何产生的?这是我在学习 Python 过程中,不断编写样例代码、积累点滴经验,最终形成的一套系统教程。
Python 是一门非常优秀的编程语言,它语法简洁、可读性强、功能强大,广泛应用于 Web 开发、数据分析、机器学习、自动化脚本、人工智能等领域。对于初学者来说,Python 是入门首选语言——它的语法接近自然语言,学习曲线平缓。
Hello,Python 是一个绝佳的学习起点。通过这个项目,你不仅能快速入门 Python 编程,还能通过编写、调试、运行示例代码,迅速掌握 Python 的核心知识点。它涵盖了从基础语法到高级进阶知识的完整学习路径,包括异步编程、Web 框架(FastAPI)、数据库操作、NumPy 数值计算等内容。
本书使用 Python 3.10+ 编写(测试环境为 Python 3.13.3),包管理器使用 uv,文档使用 mdBook 构建。请查看 Getting Started 了解如何安装和配置开发环境。
Introduction (介绍)
Python 是一种高级、解释型、通用编程语言。由 Guido van Rossum 于 1991 年发布,现由 Python 软件基金会维护。Python 以"优雅、明确、简单"为设计哲学,强调代码的可读性和简洁性,支持多种编程范式:面向对象、函数式编程、过程式编程。
Python 是全球最受欢迎的编程语言之一,在 TIOBE、PYPL 等排行榜中常年位居前列。它广泛应用于 Web 开发、数据科学、机器学习、人工智能、自动化运维、网络爬虫、科学计算等领域。
Python 的设计哲学被概括为 "The Zen of Python"(Python 之禅),核心原则包括:
- 优美优于丑陋,明了优于晦涩,简洁优于复杂
- 可读性很重要 (Readability counts)
- 一种且最好只有一种方法来做一件事
为什么选择 Python?
- 易学性:语法简洁接近自然语言,非常适合编程新手入门
- 强大的生态:超过 40 万个第三方库,覆盖所有领域(Web、数据、AI、自动化…)
- 多范式支持:面向对象、函数式、过程式编程均可
- 跨平台:Windows、macOS、Linux 上均可运行
- 开源:完全免费,社区活跃
- 生产力高:几行代码即可完成其他语言数十行代码的功能
Getting Started
安装 Python
首先,你需要安装 Python。Hello Python 项目使用 Python 3.10+ 开发(推荐 3.13+)。
macOS
使用 Homebrew 安装:
$ brew install python@3.13
$ python3 --version
Python 3.13.x
Linux
使用系统包管理器安装:
# Ubuntu/Debian
$ sudo apt-get install python3 python3-pip python3-venv
# CentOS/RHEL
$ sudo yum install python3 python3-pip
Windows
从 Python 官方网站 下载并安装。安装时记得勾选 "Add Python to PATH"。
安装 uv 包管理器
Hello Python 使用 uv 作为包管理器,它比 pip + venv 更快更简洁。
# 安装 uv
$ curl -LsSf https://astral.sh/uv/install.sh | sh
# 验证安装
$ uv --version
uv x.x.x
克隆项目
$ git clone https://github.com/savechina/hello-python.git
$ cd hello-python
安装依赖
# 同步项目依赖
$ uv sync
# 验证 Python 版本
$ python --version
Python 3.13.x
运行示例
# 运行任意示例文件
$ python -m hello_python.basic.datatype_sample
$ python -m hello_python.advance.json_sample
# 或通过 CLI 入口
$ uv run hello greet "World"
运行测试
# 运行所有基础教程测试
$ uv run pytest tests/basic/ -s -v
# 运行特定模块测试
$ uv run pytest tests/basic/test_datatype_sample.py -s -v
Lint 和格式化
# 代码检查
$ uv run ruff check .
# 代码格式化
$ uv run ruff format .
构建文档
Hello Python 使用 mdBook 构建文档。你可以本地预览教程:
# 构建文档
$ mdbook build docs
# 本地启动文档服务
$ mdbook serve docs
文档将在 http://localhost:3000 打开。
项目结构
hello-python/
├── hello_python/ # 教程源码
│ ├── basic/ # 基础教程(变量、数据类型、循环、函数…)
│ ├── advance/ # 进阶教程(异步、FastAPI、数据库、NumPy…)
│ ├── cli/ # Click CLI 入口
│ ├── algo/ # 算法示例
│ └── leetcode/ # LeetCode 解题
├── tests/ # 测试代码(镜像 hello_python 结构)
├── docs/ # mdBook 文档
│ └── src/ # 文档源文件
│ ├── basic/ # 基础教程章节
│ └── advance/ # 进阶教程章节
├── pyproject.toml # 项目配置和依赖
└── Makefile # 快捷命令
运行 Makefile 快捷命令
$ make install # uv sync
$ make test # pytest -s -v
$ make lint # ruff check .
$ make format # ruff format .
$ make build # uv build
下一步
完成环境配置后,你可以进入 介绍 了解更多关于 Python 的信息,或者直接开始 基础入门 的学习之旅。
基础入门 (Basic Overview)
欢迎学习 Python 基础教程!本系列涵盖 Python 编程的核心概念,配合可运行的代码示例和练习题,让你在动手实践中快速入门。
学习路径
我们推荐按以下顺序学习,每章内容建立在前一章的基础之上:
| # | 章节 | 难度 | 预计时间 | 描述 |
|---|---|---|---|---|
| 1 | 变量与表达式 | ⭐ | 15 分钟 | 学习变量赋值和算术运算 |
| 2 | 基础数据类型 | ⭐ | 15 分钟 | 掌握字符串、数字、列表、字典 |
| 3 | 流程控制 | ⭐ | 15 分钟 | if/elif/else 分支与 match/case |
| 4 | 循环结构 | ⭐ | 15 分钟 | for/while 循环、enumerate、zip |
| 5 | 函数基础 | ⭐⭐ | 20 分钟 | def、参数、lambda、作用域 |
| 6 | 列表与字典 | ⭐⭐ | 20 分钟 | 推导式、集合、元组解构 |
| 7 | 文件操作 | ⭐⭐ | 20 分钟 | 读写文件、with、pathlib |
| 8 | 异常处理 | ⭐⭐ | 15 分钟 | try/except、自定义异常 |
| 9 | 模块与包 | ⭐⭐ | 15 分钟 | import、name、all |
| 10 | 面向对象编程 | ⭐⭐⭐ | 25 分钟 | class、继承、魔术方法 |
| 11 | 字符串高级处理 | ⭐⭐⭐ | 20 分钟 | 正则、split/join、f-string 高级 |
[!TIP] 全部 11 章预计学习时长约 3 小时。每章都配有练习题和自测题。
阶段复习
完成所有章节后,别忘了到 阶段复习 检验学习成果——那里有综合练习和自测题库,帮助你查漏补缺。
前置知识
无需任何编程基础。本教程从零开始,适合完全零基础的初学者。
下一步
从 变量与表达式 开始你的 Python 之旅!
变量与表达式 (Variables & Expressions)
导语
想象你在超市结账——购物车里的商品数量、单价、折扣、最终金额,这些数字都需要通过变量存储和表达式计算。Python 编程的第一步,就是学会如何让计算机"记住"数据并对其进行计算。本节将带你掌握变量和表达式,这是所有 Python 程序的基石。
学习目标
- 了解 Python 中变量的概念和基本赋值方式
- 掌握常见算术运算符(
+、-、*、/、%) - 学会使用 f-string 进行字符串格式化
概念介绍
在 Python 中,你不需要声明变量类型。当你给一个变量赋值时,Python 会自动推断其类型。例如 a = 1 — Python 知道 a 是整数。这种特性让 Python 非常适合快速原型开发。
表达式(expression)是由值和运算符组成的组合,计算机可以求值(evaluate)并返回结果。例如 4 * 30 是一个表达式,求值结果为 120。
[!NOTE] Python 中的赋值使用
=符号,而相等判断使用==,初学者经常混淆两者。
代码示例
示例 1:基本算术运算
a = 1
b = 2
c = a + b
print("c result:" + str(c)) # 输出: c result:3
加法运算直接对变量求值。注意 print() 中需要使用 str() 将数字转为字符串后再拼接。
示例 2:运算符速查
sum_val = 5 + 10 # 加法
difference = 95.5 - 4.3 # 减法
product = 4 * 30 # 乘法
quotient = 56.7 / 32.2 # 除法(结果始终为浮点数)
remainder = 43 % 5 # 求余(取模)
print(
f"sum: {sum_val}, diff: {difference}, product: {product}, quotient: {quotient}, remainder:{remainder}"
)
注意除法 / 在 Python 中始终返回浮点数(4 / 2.0 结果是 2.0 而非 2)。
示例 3:字符串格式化
word = "World"
s2 = f"Format string, Hello {word}. 你好,世界。!"
print(s2)
f-string 是 Python 3.6+ 推荐的格式化方式。在字符串前加 f,用 {} 包裹变量或表达式,即可嵌入值。
常见错误与解决
[!WARNING] 错误 1:类型混用导致报错
"结果是" + 5会抛出TypeError,因为字符串和整数不能直接拼接。解决:使用
str(5)转字符串,或改用 f-string:f"结果是 {5}"。
[!WARNING] 错误 2:除法结果类型不符合预期
5 / 2在 Python 中返回2.5(浮点数),而非2(整数)。解决:如需整数除法,使用
//:5 // 2返回2。
最佳实践
- 优先使用 f-string 而非
%或.format()— 更易读且性能更好 - 变量名要有意义 — 用
total_price而非a,用user_name而非n - 除法要注意类型 — 用
/得到浮点数,用//得到整数
练习
- 写一个表达式,计算 1 到 100 的自然数之和。
查看答案
total = (1 + 100) * 100 // 2
print(f"1-100 的和: {total}") # 5050
- 用 f-string 输出一段信息:
你今年 25 岁了,其中 25 是变量。
查看答案
age = 25
print(f"你今年 {age} 岁了")
知识检查
-
以下哪段代码会抛出
TypeError?- A.
a = 1 + 2 - B.
"结果为:" + 5 - C.
b = 10 / 2 - D.
c = f"{5 + 3}"
- A.
-
5 // 2的结果是?- A.
2.5 - B.
2 - C.
3 - D.
5
- A.
-
在 Python 中,变量在使用前需要先声明吗?
- A. 需要,用
var声明 - B. 需要,用
let声明 - C. 不需要,直接赋值即可
- D. 不确定
- A. 需要,用
查看答案
- B — 字符串和整数不能直接拼接
- B —
//是整数除法,结果为2 - C — Python 是动态类型语言,赋值即声明
本章小结
- 变量不需要声明类型,赋值即创建
- 算术运算符包括
+、-、*、/、%、// /运算符始终返回浮点数- f-string 是字符串格式化的推荐方式
- 变量命名要有意义,避免单字母名称
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| variable | 变量 | 存储数据的名称 |
| operator | 运算符 | 执行计算的符号 |
| expression | 表达式 | 由值和运算符组合的式子 |
| f-string | f-string | 字符串格式化方式 |
| literal | 字面量 | 直接写在代码中的值 |
| assignment | 赋值 | 用 = 给变量设定数值 |
下一步
- 基础数据类型 → 了解 Python 的各种数据类型(字符串、列表、字典…)
源码链接
基础数据类型 (Data Types)
导语
在编程世界里,数据是一切的基础——而数据类型决定了你可以对数据做什么、不能做什么。Python 提供了丰富的内置数据类型:文本有字符串(string),整数有整型(int),小数有浮点型(float),真/假有布尔型(bool),还有强大的列表(list)和字典(dict)。本节带你逐一认识它们。
学习目标
- 掌握 Python 最常见的几种数据类型
- 学会使用字符串的常用方法(拼接、格式化、大小写转换)
- 了解 f-string 和
.format()的区别
概念介绍
Python 是强类型语言(strongly typed)——每个值都有明确的类型,你不能随意混用不同类型的值。但同时它也是动态类型语言(dynamically typed)——你无需提前声明变量类型,Python 会在赋值时自动推断。
常见内置类型包括:
str:字符串int:整数float:浮点数bool:布尔值list:有序集合dict:键值对映射
代码示例
示例 1:字符串定义与使用
text = "hello world. sample"
print(text) # 输出: hello world. sample
字符串可以使用单引号 'hello'、双引号 "hello" 甚至三引号 '''多行字符串''' 来定义。
示例 2:字符串格式化
word = "World"
s2 = f"Format string, Hello {word}. 你好,世界。!"
print(s2)
s3 = "The sum of 1 + 2 is {0}".format(1 + 2)
print(s3)
[!TIP] f-string(Python 3.6+)是推荐方式。
.format()适用于动态模板场景。旧式%格式化已不推荐。
示例 3:字符串方法
fo = "foo"
print("foo capitalize:" + fo.capitalize()) # Foo capitalize: Foo
字符串提供了大量内置方法:.upper()、.lower()、.strip()、.split()、.replace() 等等,是日常编程中使用频率最高的 API 之一。
常见错误与解决
[!WARNING] 错误 1:用
+拼接大量字符串导致性能问题在循环中反复用
+拼接字符串会产生大量中间对象。解决:使用
''.join(list_of_strings),或者用 f-string 一次性构建。
[!WARNING] 错误 2:字符串和数字混用
"价格是" + 100会报错。解决:
f"价格是 {100}"或"价格是" + str(100)。
最佳实践
- 统一使用双引号 定义字符串(项目已配置 ruff 规则)
- 优先 f-string 进行格式化
- 善用内置方法 而非自己造轮子(如
title()、strip())
练习
- 将字符串
"hello world"中的每个单词首字母大写。
查看答案
text = "hello world"
print(text.title()) # Hello World
- 提取文件名
"report_2024_final.pdf"的扩展名(不含.)。
查看答案
filename = "report_2024_final.pdf"
extension = filename.rsplit(".", 1)[1]
print(extension) # pdf
知识检查
-
"hello".capitalize()返回?- A.
HELLO - B.
Hello - C.
hello - D.
HELLO WORLD
- A.
-
以下哪种字符串格式化方式性能最好?
- A.
"Name: %s" % name - B.
"Name: {}".format(name) - C.
f"Name: {name}" - D. 性能没有差异
- A.
-
Python 中如何定义多行字符串?
- A. 使用
// - B. 使用三引号
"""...""" - C. 使用
\n拼接 - D. 无法定义
- A. 使用
查看答案
- B —
.capitalize()将首字母大写 - C — f-string 是编译期优化,性能最佳
- B — 三引号保留换行
本章小结
- Python 是强类型但动态类型语言
- 字符串可以用单引号、双引号或多引号定义
- f-string 是最推荐的格式化方式
- 字符串内置方法丰富,善用
.split()、.join()、.strip()等 - 注意类型混用会抛出
TypeError
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| string (str) | 字符串 | 文本数据类型 |
| integer (int) | 整数 | 不带小数点的数字 |
| float | 浮点数 | 带小数点的数字 |
| boolean (bool) | 布尔值 | True 或 False |
| list | 列表 | 有序可变集合 |
| dict | 字典 | 键值对映射 |
下一步
- 流程控制 → 学会根据条件做出不同的程序分支
源码链接
流程控制 (Control Flow)
导语
生活中无时无刻在做决定:如果下雨,我就带伞;如果迟到,我就打车。程序也一样——没有流程控制的代码只是一条直线。Python 提供了 if/elif/else 分支、三元运算符(ternary operator)、以及 Python 3.10+ 引入的 match/case 结构模式匹配。掌握这些,你的程序才能"思考"。
学习目标
- 掌握
if/elif/else条件分支 - 学会使用 Python 三元运算符简化代码
- 了解
match/case模式匹配语法(Python 3.10+)
概念介绍
流程控制(control flow)决定了代码的执行路径。Python 最常见的流程控制是条件分支:根据某个条件的真假(True 或 False),决定执行哪一段代码。
Python 中 falsy(假值)包括:False、None、0、""(空字符串)、[](空列表)、{}(空字典)。其余值均为 truthy(真值)。
代码示例
示例 1:if/elif/else 分支
score = 85
if score >= 90:
grade = "A (优秀)"
elif score >= 80:
grade = "B (良好)"
elif score >= 70:
grade = "C (及格)"
elif score >= 60:
grade = "D (待提高)"
else:
grade = "F (不及格)"
print(f"分数 {score} 对应的等级: {grade}")
# 输出: 分数 85 对应的等级: B (良好)
注意 Python 中没有 switch 语句——在 3.10 之前,elif 链就是多路分支的标准写法。
示例 2:三元运算符
temperature = 35
weather = "炎热" if temperature > 30 else "舒适"
print(f"温度 {temperature}°C, 感觉: {weather}")
# 输出: 温度 35°C, 感觉: 炎热
Python 的三元运算符格式为 value_if_true if condition else value_if_false,可读性优于传统 ? :。
示例 3:match/case(Python 3.10+)
status_code = 404
match status_code:
case 200:
message = "OK - 请求成功"
case 301 | 302:
message = "重定向 (Redirect)"
case 404:
message = "Not Found - 页面未找到"
case 500:
message = "Server Error - 服务器错误"
case _:
message = f"Unknown status code: {status_code}"
print(f"HTTP {status_code}: {message}")
# 输出: HTTP 404: Not Found - 页面未找到
case _ 是通配匹配,相当于 else 或 default。
[!NOTE]
match/case不仅仅是 switch 的替代品——它支持结构模式匹配(structural pattern matching),可以匹配数据类型、解构列表/字典等。详见 Python 官方文档。
常见错误与解决
[!WARNING] 错误 1:混淆
=和==
if a = 1:会报SyntaxError,因为=是赋值而非比较。解决:使用
==做相等判断。
[!WARNING] 错误 2:
if x == True冗余不需要写
if x == True:——直接写if x:即可。同理if x == False:应写为if not x:。
最佳实践
- 优先三元运算符 替代简单的
if/else赋值 match/case适合多路分支(3 个以上条件),比elif链更清晰- 避免深层嵌套 — 超过 3 层嵌套时考虑提前 return 或提取函数
练习
- 写一个函数,判断一个数字是正数、负数还是零,返回对应字符串。
查看答案
def check_number(n):
if n > 0:
return "正数"
elif n < 0:
return "负数"
else:
return "零"
print(check_number(-5)) # 负数
- 用三元运算符判断一个字符串是否为空(空字符串返回
"空",否则返回"非空")。
查看答案
text = ""
result = "空" if not text else "非空"
print(result) # 空
知识检查
-
以下代码输出什么?
x = 0 if x: print("truthy") else: print("falsy")- A.
truthy - B.
falsy - C. 报错
- D. 无输出
- A.
-
match/case从哪个 Python 版本开始提供?- A. 3.6
- B. 3.8
- C. 3.10
- D. 3.12
-
三元运算符
a if cond else b中,当cond为True时,返回?- A.
a - B.
b - C.
cond - D.
True
- A.
查看答案
- B —
0是 falsy,进入else分支 - C — Python 3.10 引入
match/case - A — 条件为真时返回
a
本章小结
if/elif/else是 Python 最基础的条件分支- Python 没有传统
switch——用elif链或match/case - 三元运算符
a if cond else b适合简单分支赋值 match/case支持类型匹配和模式解构- falsy 值包括
False、None、0、""、[]、{}
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| conditional | 条件 | 控制程序分支的判断 |
| if statement | if 语句 | 最基本条件分支 |
| elif | elif | else if 的缩写 |
| else | else | 默认分支 |
| ternary operator | 三元运算符 | 一行条件表达式 |
| match/case | match/case | 结构模式匹配 |
下一步
- 循环结构 → 学会重复执行代码块
源码链接
循环结构 (Loops)
导语
重复是程序员最强大的美德之一——但不是让你手动复制粘贴代码。Python 提供了 for 循环和 while 循环来处理重复任务,配合 enumerate()、zip() 和列表推导式(comprehension),能让循环代码既简洁又优雅。学会循环,你就掌握了自动化的第一步。
学习目标
- 掌握
for循环遍历可迭代对象 - 学会
while循环及其适用场景 - 理解
break、continue和else子句 - 熟练使用
enumerate()和zip()增强循环 - 了解列表推导式基础
概念介绍
循环(loop)让程序可以重复执行一段代码块,直到满足退出条件。Python 中最常用的是 for 循环(for loop)——它遍历一个可迭代对象(iterable),每次取出一个元素执行循环体。
while 循环(while loop)则在条件为 True 时持续执行,适合"不知道要循环多少次"的场景。循环体中可以嵌套 if 判断,实现更复杂的逻辑。
Python 的循环有一个独特特性:循环可以带 else 子句——只有循环正常结束(没有遇到 break)时才会执行 else 块。这为"遍历未找到"的场景提供了优雅的写法。
[!TIP] 如果你熟悉 C/Java 的
for (int i = 0; i < n; i++),Python 的for i in range(n)是等价写法,但更直观——"对于range(n)中的每一个i"。
代码示例
示例 1:for 循环 — range() 与列表遍历
# 使用 range() 控制循环次数
for i in range(3):
print(f"第 {i} 次循环")
# 输出: 第 0 次循环, 第 1 次循环, 第 2 次循环
# 直接遍历列表
fruits = ["苹果", "香蕉", "樱桃"]
for fruit in fruits:
print(f"我喜欢吃 {fruit}")
# 输出: 我喜欢吃 苹果, 我喜欢吃 香蕉, 我喜欢吃 樱桃
range(n) 生成 0 到 n-1 的整数序列。range(start, stop, step) 可以指定起始、结束和步长。
示例 2:while 循环与 break/continue
# while + break:找到目标就退出
numbers = [1, 3, 7, 4, 8]
target = 7
for num in numbers:
if num == target:
print(f"找到了 {target}!")
break
print(f"检查 {num},不是目标")
# continue:跳过偶数
for i in range(6):
if i % 2 == 0:
continue # 跳过本次循环剩余代码
print(f"奇数: {i}")
# 输出: 奇数: 1, 奇数: 3, 奇数: 5
[!NOTE]
break跳出整个循环,continue跳过当前迭代进入下一次循环。不要把两者混淆。
示例 3:循环 else 子句
numbers = [1, 3, 5, 7, 9]
target = 4
for num in numbers:
if num == target:
print(f"找到了 {target}")
break
else:
print(f"{target} 不在列表中")
# 输出: 4 不在列表中
只有循环没有被 break 中断时,else 块才执行。这在"搜索"场景中非常有用——找到了用 break 退出,没找到走 else。
示例 4:enumerate() 与 zip()
# enumerate:同时获取索引和值
colors = ["红色", "绿色", "蓝色"]
for index, color in enumerate(colors):
print(f"第 {index} 个颜色: {color}")
# zip:并行遍历多个序列
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]
for name, age in zip(names, ages):
print(f"{name} {age} 岁")
# 输出: Alice 30 岁, Bob 25 岁, Charlie 35 岁
enumerate(iterable) 返回 (索引, 值) 的迭代器。zip(a, b) 将多个序列"拉链式"配对。
[!TIP]
zip()在序列长度不同时,会以最短的为准截断。如需完整遍历,使用itertools.zip_longest()。
示例 5:列表推导式基础
# 传统写法
squares = []
for x in range(5):
squares.append(x ** 2)
# 列表推导式(推荐)
squares = [x ** 2 for x in range(5)]
print(squares) # [0, 1, 4, 9, 16]
# 带条件的推导式
evens = [x for x in range(10) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
列表推导式(list comprehension)将 for + append 压缩为一行,同时保持可读性。
常见错误与解决
[!WARNING] 错误 1:在循环中修改正在遍历的列表
nums = [1, 2, 3, 4] for n in nums: if n == 2: nums.remove(n) # 💥 会跳过下一个元素解决:遍历副本
for n in nums[:]或使用列表推导式[n for n in nums if n != 2]。
[!WARNING] 错误 2:无限循环
while True: print("停不下来") # 💥 永远不会停解决:确保
while条件最终能变为False,或内部有break。
[!WARNING] 错误 3:range() 边界搞错
range(5)生成 0 到 4(不含 5),不是 1 到 5。这是新手最常犯的错误。解决:从 1 开始用
range(1, 6)。
最佳实践
- 优先 for 循环 遍历可迭代对象,需要条件控制再用 while
- 善用 enumerate() 替代手动维护计数器
i = 0; for ...; i += 1 - 用列表推导式 替代简单的 for + append,保持代码简洁
- 避免嵌套超过 2 层 的循环——考虑提取函数或使用生成器
- 循环 else 子句 适合搜索场景,但不要滥用——可读性优先
练习
- 写一个 for 循环,打印 1 到 10 之间所有能被 3 整除的数字。
查看答案
for i in range(1, 11):
if i % 3 == 0:
print(i)
# 输出: 3, 6, 9
- 使用
enumerate()找出列表["a", "b", "c", "b", "d"]中"b"出现的所有索引位置。
查看答案
items = ["a", "b", "c", "b", "d"]
indices = [i for i, val in enumerate(items) if val == "b"]
print(indices) # [1, 3]
知识检查
-
range(2, 8, 2)生成哪些数字?- A.
2, 4, 6, 8 - B.
2, 4, 6 - C.
2, 3, 4, 5, 6, 7 - D.
2, 4, 6, 7
- A.
-
以下代码输出什么?
for i in range(3): if i == 2: break else: print("loop complete")- A.
loop complete - B. 无输出
- C. 报错
- D.
loop complete打印 3 次
- A.
-
zip([1, 2], ['a', 'b', 'c'])返回几个配对?- A. 3 个 —
(1, 'a'), (2, 'b'), (None, 'c') - B. 2 个 —
(1, 'a'), (2, 'b') - C. 报错
- D. 1 个
- A. 3 个 —
查看答案
- B —
range(start, stop, step)不包含 stop,步长为 2 - B —
break中断了循环,else子句不执行 - B —
zip()以最短序列为准
本章小结
for循环遍历可迭代对象,while基于条件重复执行break跳出循环,continue跳过当前迭代- 循环
else子句在循环正常结束时执行(未被break) enumerate()提供索引-值对,zip()并行配对多个序列- 列表推导式
[expr for x in iterable if cond]是 Pythonic 写法
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| for loop | for 循环 | 遍历可迭代对象的循环 |
| while loop | while 循环 | 基于条件判断的循环 |
| break | break | 跳出整个循环 |
| enumerate | enumerate | 同时获取索引和值的内置函数 |
| zip | zip | 并行遍历多个序列的内置函数 |
下一步
- 函数基础 → 学会封装可复用代码块
源码链接
函数基础 (Functions)
导语
函数(function)是将代码"打包"的魔法——把一段可复用的逻辑封装起来,需要时调用即可。从 print() 到 len(),你每天用的内置函数都是别人写好的函数。现在,轮到你自己写了。Python 的函数系统相当强大:默认参数、*args/**kwargs、lambda 表达式、以及独特的 LEGB 变量作用域规则,掌握这些,你的代码就能从"脚本"进化为"工程"。
学习目标
- 使用
def定义函数并传递参数 - 理解位置参数(positional)与关键字参数(keyword)
- 掌握
*args和**kwargs处理任意参数 - 学会 lambda 匿名函数
- 理解 LEGB 变量作用域规则
概念介绍
函数是一段可重复调用的代码块。在 Python 中使用 def(define)关键字定义函数。函数可以接收参数(parameter)、也可以返回值。没有 return 语句的函数默认返回 None。
Python 的函数系统有几个独特之处:
- 参数可以有默认值 — 调用时可以省略,提高了灵活性
*args接收任意数量的位置参数 — 打包为元组(tuple)**kwargs接收任意数量的关键字参数 — 打包为字典(dict)- 变量作用域遵循 LEGB 规则 — Local → Enclosing → Global → Built-in
[!NOTE] Python 中一切皆对象,函数也不例外。函数可以赋值给变量、作为参数传递、也可以从函数返回——这就是所谓的一等函数(first-class function)。
代码示例
示例 1:def 定义函数与参数
def greet(name, greeting="Hello"):
"""带默认参数的函数。"""
return f"{greeting}, {name}!"
print(greet("Python")) # Hello, Python!
print(greet("Python", "你好")) # 你好, Python!
greeting="Hello" 是默认参数(default parameter)。调用时如果没传 greeting,就使用默认值。
[!WARNING] 默认参数只能放在必选参数之后!
def greet(greeting="Hello", name):会报SyntaxError。
示例 2:返回值
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8
# Python 支持返回多个值(实际是返回元组)
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 1, 7, 2, 9])
print(low, high) # 1 9
没有 return 的函数隐式返回 None。
示例 3:*args 与 **kwargs
def show_info(*args, **kwargs):
print("位置参数:", args)
print("关键字参数:")
for key, value in kwargs.items():
print(f" {key} = {value}")
show_info("Python", "3.13", version="3.13", type="tutorial")
# 位置参数: ('Python', '3.13') ← args 是元组
# 关键字参数:
# version = 3.13 ← kwargs 是字典
# type = tutorial
*args 将额外位置参数打包为元组,**kwargs 将额外关键字参数打包为字典。
[!TIP] 参数顺序规则:
def func(positional, *args, default=value, **kwargs)— 位置参数 →*args→ 默认参数 →**kwargs。
示例 4:lambda 匿名函数
# 普通函数
def square(x):
return x * x
# 等价 lambda
square = lambda x: x * x
print(square(5)) # 25
# lambda 常见于排序 key
students = [("Bob", 90), ("Alice", 85), ("Charlie", 95)]
students.sort(key=lambda s: s[1], reverse=True)
print(students) # [('Charlie', 95), ('Bob', 90), ('Alice', 85)]
lambda 只能包含一个表达式,不能有语句。适合短小的、一次性的函数。
示例 5:LEGB 作用域规则
x = "global" # Global 作用域
def outer():
x = "enclosing" # Enclosing 作用域
def inner():
x = "local" # Local 作用域
print(f"inner: {x}") # local
inner()
print(f"outer: {x}") # enclosing
outer()
print(f"global: {x}") # global
# Builtin 作用域:内置函数如 len(), print() 等
LEGB 规则:Python 查找变量时按 Local → Enclosing → Global → Built-in 的顺序逐层查找,找到即停。
[!NOTE] 如果要在函数内修改全局变量,需要使用
global关键字;修改 enclosing 作用域的变量,使用nonlocal。
常见错误与解决
[!WARNING] 错误 1:可变默认参数的陷阱
def add_item(item, target=[]): target.append(item) return target print(add_item(1)) # [1] print(add_item(2)) # [1, 2] 💥 为什么不是 [2]?原因:默认参数的值在函数定义时求值一次,而非每次调用时创建。
解决:使用
None作为默认值。def add_item(item, target=None): if target is None: target = [] target.append(item) return target
[!WARNING] 错误 2:忘记 return
def add(a, b): a + b # 💥 没有 return!返回 None result = add(3, 5) print(result) # None解决:确保需要返回值的函数有
return语句。
[!WARNING] 错误 3:关键字参数位置错误
greet("Python", "Hello", name="World") # 💥 error解决:位置参数必须在关键字参数之前。要么全部位置,要么全部关键字。
最佳实践
- 函数职责单一 — 一个函数只做一件事,超过 30 行考虑拆分
- 参数不要超过 5 个 — 参数太多时考虑用字典或 dataclass 传参
- 避免可变默认参数 — 默认参数用
None,内部再创建空容器 - 写 docstring — 每个函数第一行用三引号字符串描述功能、参数、返回值
- 优先具名函数 — lambda 适合简单场景,复杂逻辑用
def更易维护
练习
- 写一个函数
calculate(a, b, operation="add"),支持"add"、"subtract"、"multiply"、"divide"四种运算,默认加法。
查看答案
def calculate(a, b, operation="add"):
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
return a / b if b != 0 else "除数不能为零"
else:
return f"未知操作: {operation}"
print(calculate(10, 5)) # 15
print(calculate(10, 5, "divide")) # 2.0
- 用 lambda 对列表
["banana", "Apple", "cherry"]按忽略大小写字母排序。
查看答案
words = ["banana", "Apple", "cherry"]
words.sort(key=lambda w: w.lower())
print(words) # ['Apple', 'banana', 'cherry']
知识检查
-
以下代码输出什么?
def f(x, y=[]): y.append(x) return y print(f(1)) print(f(2))- A.
[1],[2] - B.
[1],[1, 2] - C.
[1],[2, 1] - D. 报错
- A.
-
*args在函数内部是什么类型?- A. 列表(list)
- B. 元组(tuple)
- C. 字典(dict)
- D. 集合(set)
-
以下哪个是合法的 lambda 表达式?
- A.
lambda x: x + 1 - B.
lambda x: return x + 1 - C.
lambda x: {return x + 1} - D.
lambda (x): return x + 1
- A.
查看答案
- B — 可变默认参数被多次调用共享
- B —
*args打包为元组 - A — lambda 只能包含表达式,不能有
return关键字
本章小结
def定义函数,可带位置参数、默认参数和返回值*args接收任意位置参数(元组),**kwargs接收任意关键字参数(字典)- lambda 适合简单的一次性函数,语法
lambda 参数: 表达式 - LEGB 规则(Local → Enclosing → Global → Built-in)决定变量查找顺序
- 永远不要用可变对象(列表、字典)作为默认参数
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| function | 函数 | 封装可复用代码的块 |
| parameter | 参数 | 函数定义时的占位变量 |
| keyword argument | 关键字参数 | 用 name=value 传递的参数 |
| lambda | lambda | 匿名一行函数 |
| scope (LEGB rule) | 作用域 (LEGB 规则) | 变量查找顺序规则 |
下一步
- 列表与字典 → 掌握最常用的数据结构
源码链接
列表与字典 (Lists & Dicts)
导语
想象你在整理一张购物清单——你要不断添加新商品、删掉已买的、修改价格信息。这就是列表和字典的日常用途:它们是 Python 中最常用、最灵活的数据容器。列表(list)是有序的"排队"结构,字典(dict)是键值对应的"查表"结构。掌握它们,加上列表推导式(comprehension)和集合(set),你就能优雅地处理绝大多数数据组织问题。
学习目标
- 掌握列表推导式(list comprehension)的简单、条件、嵌套三种写法
- 学会字典的
get()、items()、update()常用方法 - 理解集合(set)运算和元组(tuple)解包(unpacking)
概念介绍
列表推导式是一种用一行表达式生成列表的 Pythonic 写法。它的核心思想是:把 for 循环和条件判断压缩为一个表达式,同时避免显式的 .append() 调用。
字典是键值对映射(key-value mapping),通过键快速查找值,底层基于哈希表(hash table)实现,所以查找复杂度为 O(1)。
集合(set)是无序不重复元素的容器,天然支持数学集合运算:并集(union)、交集(intersection)、差集(difference)。元组(tuple)是不可变的有序序列,常用于解包(unpacking)——一次性把多个值赋给多个变量。
[!TIP] 列表推导式比等效的
for + append快约 20%–30%,因为它们在 C 层面优化了内存分配。
代码示例
示例 1:列表推导式(简单、条件、嵌套)
# 简单推导式:生成 0-4 的平方
squares = [x * x for x in range(5)]
print(f"squares of 0-4: {squares}")
# 输出: squares of 0-4: [0, 1, 4, 9, 16]
# 带条件的推导式:筛选偶数
evens = [x for x in range(10) if x % 2 == 0]
print(f"evens 0-9: {evens}")
# 输出: evens 0-9: [0, 2, 4, 6, 8]
# 嵌套推导式:生成 3x3 乘法表矩阵
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(f"3x3 multiplication matrix: {matrix}")
# 输出: 3x3 multiplication matrix: [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
[!NOTE] 嵌套列表推导式的读法是从外到内:外层
for i in range(1, 4)控制行,内层for j in range(1, 4)控制列。
示例 2:字典的 get()、items()、update()
user = {"name": "Alice", "age": 30, "city": "Shanghai"}
# 安全访问:get() 找不到键时返回 None 而非 KeyError
print(f"name: {user.get('name')}") # Alice
print(f"missing key (None): {user.get('phone')}") # None
# 遍历所有键值对
for key, value in user.items():
print(f" {key}: {value}")
# 合并/更新:update() 就地修改字典
user.update({"email": "alice@example.com"})
print(f"after update: {user}")
[!WARNING] 直接用
user['phone']访问不存在的键会抛出KeyError。不确定键是否存在时,始终优先使用.get()。
示例 3:集合运算与元组解包
# 集合运算
a = {1, 2, 3}
b = {3, 4, 5}
print(f"union: {a | b}") # {1, 2, 3, 4, 5}
print(f"intersection: {a & b}") # {3}
print(f"difference: {a - b}") # {1, 2}
# 元组解包
point = (10, 20)
x, y = point
print(f"point ({x}, {y})") # point (10, 20)
[!TIP] 元组解包也可以用于交换变量:
a, b = b, a——无需临时变量,这是 Python 独有的优雅写法。
常见错误与解决
[!WARNING] 错误 1:推导式过度嵌套导致可读性下降
# 三重嵌套——几乎无法理解 result = [[i * j * k for k in range(3)] for j in range(3) for i in range(3)]解决:超过两层的推导式应该拆分为普通
for循环。可读性永远优先于简洁性。
[!WARNING] 错误 2:试图解包数量不匹配的元组
point = (10, 20, 30) x, y = point # 💥 ValueError: too many values to unpack解决:使用
*收集多余值:x, y, *rest = point,或确保两边元素数量一致。
最佳实践
- 简单的列表生成优先推导式:
[x**2 for x in items]比for + append更 Pythonic - 字典访问优先
.get():避免因键不存在触发KeyError;需要默认值时用.get(key, default) - 集合用于去重和成员关系测试:
"apple" in {"apple", "banana"}是 O(1) 操作 - 元组解包处理多返回值:
width, height = get_size()让代码意图清晰
练习
- 用列表推导式生成 1 到 20 之间所有能被 3 整除的数的平方。
查看答案
result = [x * x for x in range(1, 21) if x % 3 == 0]
print(result) # [9, 36, 81, 144, 225, 324]
- 有两个字典
d1 = {"a": 1, "b": 2}和d2 = {"b": 3, "c": 4},合并它们使得 d2 的值覆盖 d1 的同名键。
查看答案
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
# 方法 1: update()
merged = d1.copy()
merged.update(d2)
# 方法 2: Python 3.9+ 合并运算符
merged = d1 | d2
print(merged) # {'a': 1, 'b': 3, 'c': 4}
知识检查
-
[x for x in range(5) if x > 3]输出什么?- A.
[0, 1, 2, 3, 4] - B.
[4] - C.
[3, 4] - D.
[]
- A.
-
字典
d = {"a": 1},d.get("b", 0)返回什么?- A.
KeyError - B.
None - C.
0 - D.
1
- A.
-
集合
{1, 2} | {2, 3}的结果是?- A.
{1, 2, 2, 3} - B.
{2} - C.
{1, 2, 3} - D.
{1, 3}
- A.
查看答案
- B —
range(5)生成 0-4,只有 4 大于 3 - C —
.get(key, default)在键不存在时返回默认值 - C —
|是集合的并集运算,自动去重
本章小结
- 列表推导式
[expr for x in iterable if cond]是生成列表的首选方式 - 字典
.get()安全访问值,.items()遍历键值对,.update()合并字典 - 集合支持
|(并集)、&(交集)、-(差集)运算 - 元组解包
a, b = pair使多值赋值简洁优雅 - 推导式不是越复杂越好——超过两层嵌套时就拆成普通循环
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| list comprehension | 列表推导式 | 一行表达式生成列表 |
| dict comprehension | 字典推导式 | 一行表达式生成字典 |
| set | 集合 | 无序不重复元素容器 |
| tuple | 元组 | 不可变有序序列 |
| unpacking | 解包 | 将序列元素分别赋给多个变量 |
下一步
- 文件操作 → 学会读写文件和路径管理
源码链接
文件操作 (File I/O)
导语
你每天都在和文件打交道——打开一篇文档、保存一张图片、读取配置文件。在编程中,文件 I/O(file input/output)是程序与外部世界交换数据的基本方式。Python 提供了简洁而强大的文件操作 API:简单的 open()、自动管理资源的上下文管理器(context manager)、以及面向对象的 pathlib 模块。学会文件操作,你的程序才能持久化数据、读取配置、处理日志。
学习目标
- 掌握
open()三种基本模式:读取(r)、写入(w)、追加(a) - 熟练使用
with语句(上下文管理器)自动管理文件资源 - 学会使用
pathlib.Path进行现代文件系统操作
概念介绍
文件操作的核心是 open() 函数——它打开一个文件并返回文件对象。打开文件时必须指定模式(mode):"r" 只读、"w" 写入(覆盖已有内容)、"a" 追加(在末尾添加而不覆盖)。
Python 推荐始终使用 with 语句(with statement)来操作文件。with 背后是上下文管理器(context manager)协议——无论代码是否正常结束,with 都会确保文件被正确关闭,避免资源泄露。
读取大文件时,逐行读取(line-by-line)比一次性 read() 更高效——Python 的文件对象本身就是可迭代的,每次 for 循环只将一行加载到内存。
[!NOTE] Python 3.4 引入的
pathlib是目前官方推荐的现代路径操作方式。相比旧的os.path,pathlib.Path使用面向对象的链式调用,更直观、更 Pythonic。
代码示例
示例 1:open() 读写模式
# 写入文件(w 模式)
with open("hello.txt", "w") as f:
f.write("Hello, Python!\n")
f.write("你好,世界!\n")
# 读取文件(r 模式)
with open("hello.txt", "r") as f:
content = f.read()
print(f"file content:\n{content}")
# 追加内容(a 模式)
with open("hello.txt", "a") as f:
f.write("Appended line.\n")
[!NOTE]
"r"是默认模式,open("file.txt")等价于open("file.txt", "r")。"w"模式会清空已有文件内容,小心使用。
示例 2:with 语句与逐行读取
# 用 with 自动管理资源
with open("data.txt", "w") as f:
f.write("Line 1\n")
f.write("Line 2\n")
# 逐行读取——内存友好
with open("data.txt", "r") as f:
for line in f:
print(f"line: {line.strip()}")
# 输出:
# line: Line 1
# line: Line 2
[!TIP]
for line in f:是读取文件的最高效方式。相比f.readlines()(一次性加载全部行到列表),逐行读取只需维护一行内容的内存。
示例 3:pathlib.Path 现代操作
from pathlib import Path
path = Path(".")
# 检查路径是否存在
print(f"current dir exists: {path.exists()}")
# 列出当前目录下的 .py 文件
py_files = [p.name for p in path.iterdir() if p.name.endswith(".py")]
print(f"Python files in current dir: {py_files}")
# 路径拼接与文件信息
readme = path / "README.md"
if readme.exists():
print(f"README.md size: {readme.stat().st_size} bytes")
# 一步读写文件
readme_content = readme.read_text()
print(f"README length: {len(readme_content)} characters")
[!TIP]
Path / "subdir"使用/运算符拼接路径,比os.path.join()更直观。Path.read_text()和Path.write_text()可以一步完成整个文件的读写。
常见错误与解决
[!WARNING] 错误 1:忘记关闭文件
f = open("data.txt", "r") content = f.read() # 💥 忘记 f.close(),文件句柄泄露解决:始终使用
with语句,让 Python 自动关闭文件:with open("data.txt", "r") as f: content = f.read()
[!WARNING] 错误 2:写入中文时报
UnicodeEncodeError在 Windows 上,
open()默认编码可能是gbk,遇到 UTF-8 字符会报错。解决:始终显式指定编码:
open("file.txt", "w", encoding="utf-8")。
最佳实践
- 永远用
with打开文件——自动关闭、异常安全,无需手动f.close() - 大文件逐行读取——
for line in f:比f.read()或f.readlines()更节省内存 - 优先
pathlib——Path.read_text()一行完成读写,/运算符拼接路径优雅清晰 - 显式指定
encoding——避免平台差异导致的编码问题
练习
- 使用 pathlib 找出当前目录下所有
.md文件,并打印它们的大小(字节)。
查看答案
from pathlib import Path
for p in Path(".").iterdir():
if p.suffix == ".md":
print(f"{p.name}: {p.stat().st_size} bytes")
- 读取一个日志文件
app.log,只打印包含"ERROR"的行。
查看答案
with open("app.log", "r", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
print(line.strip())
知识检查
-
open("file.txt", "a")的作用是?- A. 清空文件重新写入
- B. 在文件末尾追加内容
- C. 只读模式打开
- D. 删除文件
-
关于
with open(...)以下说法正确的是?- A. 需要手动调用
.close() - B. 只读取文件的一部分
- C. 代码结束后自动关闭文件
- D. 仅二进制文件需要
- A. 需要手动调用
-
Path(".").iterdir()返回什么?- A. 一个字符串列表
- B. 当前目录下的 Path 对象迭代器
- C. 布尔值
- D. 文件内容
查看答案
- B —
"a"(append) 在文件末尾追加内容,不会清空已有内容 - C —
with是上下文管理器,退出时自动调用.close() - B —
iterdir()返回目录中每个条目的Path对象
本章小结
open()支持"r"(读)、"w"(写)、"a"(追加)三种基本模式with语句确保文件资源安全释放,是 Python 文件操作的标准写法for line in f:是逐行读取的高效方式,内存友好pathlib.Path提供面向对象的路径操作,/运算符拼接路径、.read_text()一步读写- 始终显式指定
encoding="utf-8"避免跨平台编码问题
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| file I/O | 文件 I/O | 文件的读写输入输出操作 |
| context manager | 上下文管理器 | 用 with 自动管理资源的协议 |
| with statement | with 语句 | 确保资源正确释放的代码块 |
| pathlib | pathlib | 面向对象的现代路径操作库 |
| file mode | 文件模式 | 打开文件的方式(r/w/a等) |
下一步
- 异常处理 → 学会优雅地处理运行错误
源码链接
异常处理 (Exception Handling)
导语
你的程序会在哪个环节出错?文件找不到、网络断开、用户输入了非法数据——程序的世界充满不确定性。Python 用 异常(exception)机制来优雅地处理这些"意外情况"。通过 try/except/finally 结构,你可以精准捕获特定错误、提供降级方案、并保证资源被正确清理。写好异常处理,你的程序就从"脆弱"变成了"坚韧"。
学习目标
- 掌握
try/except/finally/else完整结构 - 学会使用多个
except子句捕获不同类型的异常 - 掌握自定义异常类(custom exception)的定义与
raise用法
概念介绍
异常(exception)是程序运行过程中发生的问题。每个异常都是一个 Exception 的子类——ValueError(值错误)、KeyError(键不存在)、ZeroDivisionError(除零)、FileNotFoundError(文件不存在)等等。
try/except 的核心思想:把可能出错的代码放在 try 块中,用 except 捕获特定异常并处理。Python 还支持 else 子句(无异常时执行)和 finally 子句(无论如何都会执行)——常用于清理资源。
对于业务逻辑中的非法状态,你可以继承 Exception 创建自定义异常类,然后用 raise 主动抛出。这是 Python 中"显式优于隐式"原则的典型体现。
[!NOTE] Python 的异常层次是树状结构。
except Exception可以捕获大多数异常(但不包括SystemExit、KeyboardInterrupt等),而裸露的except:(无类型)会捕获一切——包括那些你不应该捕获的系统级异常。
代码示例
示例 1:try/except — 捕获特定异常
raw = "not a number"
try:
value = int(raw)
print(f"value: {value}")
except ValueError as e:
print(f"ValueError: cannot convert '{raw}' to int — {e}")
# 输出: ValueError: cannot convert 'not a number' to int — invalid literal for int()
[!NOTE]
except ValueError as e中的as e将异常对象绑定到变量e,方便在错误处理中获取详情。
示例 2:try/except/else/finally — 完整结构
numbers = [10, 2, 0]
for n in numbers:
try:
result = 100 / n
except ZeroDivisionError:
print(f" n={n}: cannot divide by zero")
else:
print(f" n={n}: 100 / {n} = {result}")
finally:
print(f" n={n}: cleanup done")
# 输出:
# n=10: 100 / 10 = 10.0 ← 成功,走 else
# n=10: cleanup done ← finally 总执行
# n=2: 100 / 2 = 50.0
# n=2: cleanup done
# n=0: cannot divide by zero ← 捕获零除异常
# n=0: cleanup done ← finally 仍然执行
[!TIP]
else子句只在没有异常时执行,把"正常逻辑"和"异常处理"分开,提高可读性。
示例 3:自定义异常与 raise
class InvalidAgeError(Exception):
"""自定义异常:年龄不合法。"""
def __init__(self, age, message="年龄必须在 0-150 之间"):
self.age = age
self.message = f"{message},当前值: {age}"
super().__init__(self.message)
def validate_age(age):
if age < 0 or age > 150:
raise InvalidAgeError(age)
print(f" age {age}: valid")
ages = [25, -3, 200]
for age in ages:
try:
validate_age(age)
except InvalidAgeError as e:
print(f" age {age}: {e}")
# 输出:
# age 25: valid
# age -3: 年龄必须在 0-150 之间,当前值: -3
# age 200: 年龄必须在 0-150 之间,当前值: 200
[!NOTE] 自定义异常类必须继承
Exception(或其子类),通过raise主动抛出。raise可以单独使用(在except块中重新抛出),也可以带参数。
常见错误与解决
[!WARNING] 错误 1:使用裸露的
except:吞掉所有异常try: do_something() except: # 💥 捕获一切,包括 KeyboardInterrupt pass解决:始终指定具体异常类型。至少要写
except Exception:而非裸except:。
[!WARNING] 错误 2:在
except中不做任何处理try: value = int(user_input) except ValueError: pass # 💥 错误被静默吞掉,无法追踪解决:至少打印日志或记录错误信息:
logging.error(f"Invalid input: {user_input}")。
最佳实践
- 精确捕获 — 只捕获你知道如何处理的具体异常类型,别用
except Exception:兜底一切 - 善用
finally清理资源 — 关闭文件、断开数据库连接等清理操作放finally,确保异常时也能执行 - 自定义异常要有意义 — 异常名要表达业务语义(如
InvalidAgeError而非MyError) - 异常信息要具体 — 在异常消息中包含出错值和上下文,方便调试
练习
- 写一个函数
divide(a, b),捕获ZeroDivisionError和TypeError,分别返回友好提示。
查看答案
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "错误:除数不能为零"
except TypeError:
return "错误:参数必须是数字"
print(divide(10, 3)) # 3.333...
print(divide(10, 0)) # 错误:除数不能为零
print(divide(10, "a")) # 错误:参数必须是数字
- 定义一个
InsufficientFundsError异常类(余额不足),在提款函数中使用raise抛出。
查看答案
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额 {balance} 不足,需提取 {amount}")
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
result = withdraw(100, 150)
except InsufficientFundsError as e:
print(e) # 余额 100 不足,需提取 150
知识检查
-
以下代码中
finally块在什么情况下执行?try: risky_operation() except ValueError: handle_error() finally: cleanup()- A. 只在没有异常时执行
- B. 只在捕获到异常时执行
- C. 无论是否有异常都执行
- D. 永远不会执行
-
自定义异常类应该继承:
- A.
RuntimeError - B.
Exception - C. 任何内置类
- D. 不需要继承
- A.
-
raise的作用是:- A. 捕获异常
- B. 抛出异常
- C. 忽略异常
- D. 定义异常类
查看答案
- C —
finally块无论如何都会执行,无论是否有异常 - B — 自定义异常应继承
Exception(不推荐直接继承BaseException) - B —
raise用于主动抛出异常
本章小结
try/except捕获特定异常,finally保证清理操作一定执行else子句在无异常时执行,分离"正常路径"和"异常路径"- 多个
except子句可分别处理不同类型的异常 - 自定义异常通过继承
Exception并实现__init__,用raise抛出 - 永远不要使用裸
except:,始终指定具体的异常类型
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| exception | 异常 | 程序运行时的错误事件 |
| try/except | try/except | 捕获并处理异常的结构 |
| finally | finally | 无论异常与否都执行的代码块 |
| custom exception | 自定义异常 | 继承 Exception 的业务专用异常类 |
| raise | raise | 主动抛出异常的语句 |
下一步
- 模块与包 → 学会组织和管理代码结构
源码链接
模块与包 (Modules & Packages)
导语
当你的代码从几十行增长到几千行,把所有东西塞在一个文件里就变成了"维护噩梦"。Python 用模块(module)和包(package)来组织代码——每个 .py 文件就是一个模块,包含 __init__.py 的目录就是一个包。import 语句让你可以在不同模块之间共享代码,if __name__ == "__main__" 让模块既能被导入又能独立运行。掌握模块系统,你的代码就从"杂乱的脚本"升级为"结构化的项目"。
学习目标
- 掌握
import和from...import两种导入方式 - 理解
if __name__ == "__main__"守卫(guard)的作用 - 学会
__all__控制模块公开接口
概念介绍
模块(module)就是单个 .py 文件。模块有自己的命名空间(namespace)——模块内的变量、函数、类不会污染全局空间。通过 import 语句,你可以访问另一个模块的内容。
import 有两种形式:
import math— 导入整个模块,使用时需加前缀math.pifrom math import pi— 只导入特定对象,使用时直接写pi
__name__ 守卫是 Python 模块系统的精髓:当一个 .py 文件被直接运行时,它的 __name__ 是 "__main__";被导入时,__name__ 是模块名。通过 if __name__ == "__main__" 可以区分这两种场景——测试代码、示例代码放在这个守卫内,被导入时不会执行。
__all__ 是一个字符串列表,定义了模块的公开 API。当其他人写 from module import *(通配导入)时,只有 __all__ 中列出的对象会被导入。这是模块化编程中"隐藏实现细节"的重要手段。
[!TIP] Python 标准库中常用的模块:
math(数学运算)、datetime(日期时间)、json(JSON 解析)、os/pathlib(文件系统)、collections(高级数据结构)。
代码示例
示例 1:import 与 from...import
import math
from datetime import datetime
# 完整导入 — 需要模块前缀
print(f"pi = {math.pi:.4f}")
# 输出: pi = 3.1416
# 部分导入 — 直接使用
now = datetime.now()
print(f"now = {now.strftime('%Y-%m-%d %H:%M')}")
# 输出: now = 2024-01-15 14:30 (具体时间)
[!NOTE]
import math和from datetime import datetime都是有效的导入方式。推荐后者——只导入你需要的东西,提高代码可读性。
示例 2:__name__ == "__main__" 守卫
def name_check():
if __name__ == "__main__":
print("This module is run directly")
else:
print("This module is imported by another module")
[!NOTE] 直接运行此文件(
python modules_packages_sample.py)时,__name__为"__main__",输出 "run directly"。被其他模块import时,__name__为"modules_packages_sample",输出 "imported by another module"。
[!TIP] 这是 Python 模块的标准模式——每个模块都可以在文件末尾加上
if __name__ == "__main__":块,放入测试代码或示例用法,让模块既可被导入又可独立运行。
示例 3:__all__ 控制公开接口
# 模块顶部定义 __all__
__all__ = ["import_basics"]
def import_basics():
"""这个函数是公开 API,可通过 __all__ 被导入。"""
print("import_basics is available")
def internal_helper():
"""这个函数是内部实现,不在 __all__ 中。"""
return "internal"
# 其他模块写 `from modules_packages_sample import *` 时
# 只能导入 import_basics,不能导入 internal_helper
[!WARNING]
__all__只影响from module import *(通配导入)的行为。对于from module import internal_helper(显式导入),__all__不起限制作用。它是对公共 API 的声明,而非强制访问控制。
常见错误与解决
[!WARNING] 错误 1:循环导入(circular import)
# a.py from b import func_b def func_a(): pass # b.py from a import func_a # 💥 ImportError: cannot import name def func_b(): func_a()解决:
- 重构——将共享代码提取到第三个模块 c.py
- 或使用局部导入:把
import放在函数内部延迟加载
[!WARNING] 错误 2:
from module import *污染命名空间通配导入会把模块的所有公共名称导入当前环境,容易产生命名冲突,且难以追踪名称来源。
解决:始终显式导入需要的对象——
from module import a, b, c,明确列出所需名称。
最佳实践
- 显式导入优于通配导入 —
from module import specific_name而非from module import * - 把测试代码放在
if __name__ == "__main__"中 — 确保模块被导入时不会执行副作用 - 用
__all__声明公开 API — 让使用者清楚哪些是稳定接口,哪些是内部实现 - 模块按功能分组 — 相关函数放在一起,保持模块"职责单一"
练习
- 假设有一个模块
utils.py,其中定义了helper()和_internal()两个函数。写一段代码,只导入helper()而不导入_internal()。
查看答案
from utils import helper
# _internal 不会被导入——即使不定义 __all__,
# 以 _ 开头的名称约定为"内部使用",不会被通配导入
helper()
- 写一个模块
math_utils.py,包含add(a, b)和multiply(a, b),并添加if __name__ == "__main__"块进行自测试。
查看答案
def add(a, b):
return a + b
def multiply(a, b):
return a * b
__all__ = ["add", "multiply"]
if __name__ == "__main__":
assert add(2, 3) == 5
assert multiply(3, 4) == 12
print("All tests passed!")
知识检查
-
当模块被直接运行时,
__name__的值是?- A. 模块文件名
- B.
"__main__" - C.
"__name__" - D.
"__init__"
-
__all__控制的是?- A.
import module导入什么 - B.
from module import *导入什么 - C. 模块能否被导入
- D. 模块内部的变量作用域
- A.
-
以下哪种导入方式是推荐的?
- A.
from math import * - B.
import math或from math import pi - C.
import * - D.
include math
- A.
查看答案
- B — 直接运行时
__name__为"__main__",被导入时为模块名 - B —
__all__只控制通配导入from module import *的行为 - B — 显式导入(完整模块或具体名称)是推荐的,通配导入应避免
本章小结
- 每个
.py文件都是一个模块,模块有自己的命名空间 import module导入整个模块,from module import name导入特定对象if __name__ == "__main__"区分"直接运行"和"被导入"两种场景__all__声明模块的公开 API,控制import *的行为- 避免
from module import *,始终显式列出需要导入的名称
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| module | 模块 | 单个 .py 文件,有独立命名空间 |
| package | 包 | 包含 __init__.py 的目录 |
| import statement | 导入语句 | 引入其他模块内容的语句 |
__name__ | __name__ | 模块名称的特殊变量 |
__all__ | __all__ | 控制公开接口的列表 |
下一步
- 面向对象编程 → 学习用类和对象组织更复杂的代码
源码链接
面向对象编程 (Object-Oriented Programming)
导语
想象你在经营一家动物园——有狗、猫、鸟等各种动物。每种动物都有名字和种类,但叫声不同、技能不同。如果不用面向对象,你需要用一堆函数和字典来管理这些动物:{"name": "旺财", "speak": "汪汪"}……但当动物种类越来越多、属性越来越复杂时,这种函数式写法会变得难以维护。面向对象编程(OOP, Object-Oriented Programming)让你用类(class)来定义动物的模板,用对象(object)来创建具体的动物,用继承(inheritance)来共享共性、覆盖差异。本节带你走进 Python 的 OOP 世界。
学习目标
- 掌握
class定义类和__init__初始化对象 - 学会继承(inheritance)与
super()调用父类 - 理解方法重写(method overriding)和
__str__/__repr__双下划线方法
概念介绍
面向对象编程(OOP)是一种编程范式(paradigm),核心思想是将数据和行为封装在同一个对象中。
在 Python 中:
- **类(class)**是对象的模板或蓝图
- **对象(object)**是根据类创建的具体实例(instance)
- **属性(attribute)**是对象上的数据成员(如
dog.name) - **方法(method)**是对象上的函数(如
dog.speak())
__init__ 是类的构造方法(constructor),在创建对象时自动调用,用于初始化对象的属性。self 指向对象自身(类似于其他语言中的 this),所有实例方法第一个参数必须是 self。
继承允许一个子类(subclass)获取父类( superclass)的属性和方法。super() 函数用于调用父类的方法,在子类的 __init__ 中特别常用。
[!NOTE] Python 所有类默认继承
object。class Animal:等价于class Animal(object):。
代码示例
示例 1:类定义与 __init__ / self
参考源码:oop_sample.py 中的 class_definition_sample()
class Animal:
"""基础动物类。"""
def __init__(self, name, species):
self.name = name # 实例属性
self.species = species # 实例属性
def speak(self):
return "..."
def __str__(self):
return f"{self.name} ({self.species})"
pet = Animal("Buddy", "Dog")
print(pet) # 调用 __str__,输出: Buddy (Dog)
class Animal:定义一个类__init__(self, name, species)是构造方法,self指向新创建的对象self.name = name将参数保存为实例属性print(pet)自动调用__str__方法获取对象的字符串表示
[!NOTE]
self不是 Python 关键字——你完全可以叫它this或其他名字,但 PEP 8 强烈推荐self。
示例 2:继承与方法重写
参考源码:oop_sample.py 中的 inheritance_sample()
class Dog(Animal):
"""Dog 继承 Animal。"""
def __init__(self, name, breed):
super().__init__(name, species="狗") # 调用父类构造方法
self.breed = breed
def speak(self):
return "汪汪!"
class Cat(Animal):
"""Cat 继承 Animal,只重写 speak。"""
def speak(self):
return "喵~"
dog = Dog("旺财", "金毛")
cat = Cat("咪咪", "猫")
for animal in [dog, cat]:
print(f" {animal.name}: {animal.speak()}")
# 旺财: 汪汪!
# 咪咪: 喵~
class Dog(Animal):表示 Dog 是 Animal 的子类super().__init__(name, species="狗")调用父类 Animal 的__init__,复用了属性初始化逻辑Dog.speak()重写(override)了父类的speak()方法,返回"汪汪!"- 这就是多态(polymorphism)——不同子类对同一个方法有不同的实现
[!TIP]
super()比直接用Animal.__init__(self, ...)更好,因为它支持多继承(multiple inheritance)的 MRO(Method Resolution Order)。
示例 3:__str__ 与 __repr__ 双下划线方法
参考源码:oop_sample.py 中的 dunder_methods_sample()
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, species="狗")
self.breed = breed
def speak(self):
return "汪汪!"
def __repr__(self):
return f"Dog('{self.name}', '{self.breed}')"
dog = Dog("大黄", "田园犬")
print(f"str: {dog}") # 调用 __str__(继承自 Animal): 大黄 (狗)
print(f"repr: {repr(dog)}") # 调用 __repr__: Dog('大黄', '田园犬')
__str__用于用户友好的字符串表示(print(obj)时使用)__repr__用于开发者友好的字符串表示(REPL 交互、调试时使用),理想情况下应像有效的 Python 代码
[!NOTE] 如果没有
__str__,print(obj)会回退到__repr__;但没有__repr__时会回退到默认的<Dog object at 0x...>。
常见错误与解决
[!WARNING] 错误 1:忘记写
self参数class Person: def greet(): # 💥 缺少 self print("Hello") Person().greet() # TypeError: greet() takes 0 positional arguments but 1 was given解决:所有实例方法的第一个参数必须是
self。
[!WARNING] 错误 2:在子类
__init__中没有调用super().__init__()class Dog(Animal): def __init__(self, name, breed): self.breed = breed # 💥 没调用 super().__init__(),self.name 不存在解决:子类的
__init__中通常要以super().__init__(...)开头初始化父类。
最佳实践
__init__只做赋值 — 不要在构造方法中做复杂计算或 I/O 操作- 优先组合而非继承 — 能用"has-a"关系的组合解决的,不用"is-a"关系的继承
- 善用
__str__和__repr__— 让自定义对象在 print 和调试中可读
练习
- 定义一个
BankAccount类,包含balance属性、deposit(amount)和withdraw(amount)方法,withdraw时检查余额是否足够。
查看答案
class BankAccount:
def __init__(self, holder, balance=0):
self.holder = holder
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须大于零")
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError(f"余额不足:余额 {self.balance},取 {amount}")
self.balance -= amount
return self.balance
def __str__(self):
return f"{self.holder}'s account: ¥{self.balance}"
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account) # Alice's account: ¥1500
- 定义一个
Student类继承Person,添加grades列表和一个average()方法返回平均分。
查看答案
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age}岁"
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age)
self.student_id = student_id
self.grades = []
def add_grade(self, grade):
self.grades.append(grade)
def average(self):
return sum(self.grades) / len(self.grades) if self.grades else 0
def __str__(self):
return f"{super().__str__()} (学号: {self.student_id})"
s = Student("小明", 15, "S001")
s.add_grade(90)
s.add_grade(85)
print(s) # 小明, 15岁 (学号: S001)
print(s.average()) # 87.5
知识检查
-
self在 Python 类方法中代表什么?- A. 当前类本身
- B. 当前实例对象
- C. 父类对象
- D. 无实际意义
-
子类调用父类的方法应该使用什么?
- A.
self.super() - B.
super() - C.
parent() - D.
base()
- A.
-
__str__和__repr__的区别是什么?- A. 没有区别,只是命名不同
- B.
__str__面向用户,__repr__面向开发者 - C.
__str__用于调试,__repr__用于打印 - D.
__str__在 Python 2 中使用,__repr__在 Python 3 中使用
查看答案
- B —
self指向调用该方法的实例对象 - B —
super()返回父类的代理对象 - B —
__str__可读性好,__repr__信息完整(理想情况可 eval)
本章小结
class定义类,__init__是构造方法,self指向实例对象- 继承用
class SubClass(ParentClass):语法,super()调用父类方法 - 方法重写(method overriding)让子类可以改变父类方法的行为
__str__提供用户友好的字符串表示,__repr__提供开发者友好的表示- OOP 的核心是封装(encapsulation)、继承(inheritance)、多态(polymorphism)
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| class | 类 | 创建对象的模板 |
| object | 对象 | 类的实例 |
| inheritance | 继承 | 子类获取父类属性和方法 |
__init__ | __init__ | 类的构造方法 |
| method override | 方法重写 | 子类重新定义父类的方法 |
下一步
- 字符串进阶 → 掌握正则表达式和高级字符串操作
源码链接
字符串进阶 (String Advanced)
导语
在真实项目中,字符串处理几乎无处不在:从日志文件中提取订单号、清洗用户输入的脏数据、验证邮箱和手机号格式……如果你只会 + 拼接和 .split(),那只能处理最简单的场景。Python 的字符串工具箱里还有更强大的武器——re 模块(regular expression,正则表达式)可以匹配复杂模式,split() / join() / strip() 可以精确操控字符串,以及 f-string 的高级格式化语法可以进行数字对齐、进制转换和精度控制。本节带你掌握 Python 字符串的高阶用法。
学习目标
- 掌握
re模块的search()、findall()、sub()三大核心函数 - 熟练运用
split()、join()、strip()等字符串方法 - 学会 f-string 的高级格式化(对齐、精度、进制)
概念介绍
正则表达式(regular expression,简称 regex)是一种用特定语法描述文本模式的语言。Python 通过内置的 re 模块提供支持。
三个最常用的 re 函数:
re.search(pattern, string)— 在字符串中搜索第一个匹配,返回 match 对象或Nonere.findall(pattern, string)— 找出字符串中所有匹配,返回列表re.sub(pattern, repl, string)— 将匹配的部分替换为指定字符串
字符串方法方面,split() 分割字符串、join() 合并序列、strip() 去除两端空白——这三个方法在日常数据处理中出场率最高。
f-string 除了基本的 f"{value}" 之外,还支持格式说明符(format specifier):比如 {number:.2f} 控制小数位数、{number:>10} 控制对齐、{number:b} 转换进制。
[!NOTE] 正则表达式的模式字符串建议用原始字符串
r"..."书写,避免反斜杠转义问题。
代码示例
示例 1:re 模块 — search / findall / sub
参考源码:string_advanced_sample.py 中的 re_module_sample()
import re
text = "Order #1234, total: ¥89.50. Order #5678, total: ¥120.00"
# findall — 提取所有订单号
orders = re.findall(r"#(\d+)", text)
print(f"order ids: {orders}")
# 输出: order ids: ['1234', '5678']
# sub — 替换匹配内容(脱敏)
redacted = re.sub(r"¥\d+\.\d{2}", "***", text)
print(f"redacted: {redacted}")
# 输出: redacted: Order #1234, total: ***. Order #5678, total: ***
# search — 查找第一个匹配并分组提取
match = re.search(r"total: ¥(\d+\.\d{2})", text)
if match:
print(f"first total: {match.group(1)}")
# 输出: first total: 89.50
正则模式解读:
#(\d+):匹配#后跟一个或多个数字,()是捕获组(capturing group)¥\d+\.\d{2}:匹配¥后跟数字、小数点、两位小数(货币格式)
[!TIP]
re.findall返回的是捕获组的内容而非完整匹配——r"#(\d+)"中的()让它只返回数字部分['1234', '5678']。
示例 2:字符串方法 — split / join / strip
参考源码:string_advanced_sample.py 中的 string_methods_sample()
csv = "name,age,city\nAlice,30,Shanghai\nBob,25,Beijing"
# strip() 去除两端空白(换行符)
lines = csv.strip().split("\n")
for line in lines:
# split(",") 按逗号分割每行
fields = line.split(",")
print(f" fields: {fields}")
# fields: ['name', 'age', 'city']
# fields: ['Alice', '30', 'Shanghai']
# fields: ['Bob', '25', 'Beijing']
# join() 合并序列——这里反序重新拼接
reversed_csv = "\n".join(line for line in reversed(lines))
print(f"reversed:\n{reversed_csv}")
[!TIP]
str.join(iterable)的性能远优于+循环拼接——它在底层一次性分配内存,而非反复创建新字符串。
示例 3:f-string 高级格式化
参考源码:string_advanced_sample.py 中的 fstring_advanced_sample()
name = "Alice"
score = 95.678
count = 42
# >10 右对齐,宽度 10
print(f"name: {name:>10}")
# 输出: name: Alice
# 8.2f 总宽度 8,保留 2 位小数
print(f"score: {score:8.2f}")
# 输出: score: 95.68
# :b 二进制,:x 十六进制
print(f"count (binary): {count:b}") # 101010
print(f"count (hex): {count:x}") # 2a
f-string 格式说明符的通用语法:{value:[fill]align;width.precision[type]}。其中:
>右对齐,<左对齐,^居中f浮点数,b二进制,x十六进制,%百分比
[!NOTE] f-string 的格式说明符在
{}内用:分隔。f"{score:8.2f}"中,8是总宽度,.2是小数位数,f是浮点类型。
常见错误与解决
[!WARNING] 错误 1:忘记用原始字符串 r"" 写正则
re.search("\d+", "abc123") # 在普通字符串中 \d 可能被视为转义 re.search(r"\d+", "abc123") # ✅ 正确:原始字符串保持 \d 原样解决:正则表达式模式始终用
r"..."(原始字符串)书写。
[!WARNING] 错误 2:split() 不传参数 vs 传空字符串
text = " hello world \n" text.split("") # 💥 ValueError: empty separator text.split() # ✅ ['hello', 'world'] — 默认按任意空白分割并去除空串解决:按空白分割时无需传参,直接
split()即可。
最佳实践
- 正则表达式用
r"..."原始字符串 — 避免反斜杠带来的转义陷阱 - 字符串拼接优先
join()— 比循环+性能更好 - 善用 f-string 格式说明符 — 对齐、精度、进制转换一行搞定
练习
- 用
re.findall从文本"Call me at 138-1234-5678 or 139-8765-4321"中提取所有手机号(格式XXX-XXXX-XXXX)。
查看答案
import re
text = "Call me at 138-1234-5678 or 139-8765-4321"
phones = re.findall(r"\d{3}-\d{4}-\d{4}", text)
print(phones) # ['138-1234-5678', '139-8765-4321']
- 用
join()和生成器表达式,将列表["apple", "banana", "cherry"]用" | "连接成大写字符串。
查看答案
fruits = ["apple", "banana", "cherry"]
result = " | ".join(f.upper() for f in fruits)
print(result) # APPLE | BANANA | CHERRY
知识检查
-
以下哪个
re函数返回所有匹配组成的列表?- A.
re.search() - B.
re.findall() - C.
re.sub() - D.
re.match()
- A.
-
" hello world ".split()的结果是?- A.
['', '', 'hello', '', 'world', '', ''] - B.
['hello', 'world'] - C.
['hello', '', 'world'] - D. 报错
- A.
-
f"{42:b}"的输出是什么?- A.
42 - B.
0b42 - C.
101010 - D.
2a
- A.
查看答案
- B —
re.findall()返回列表,re.search()返回单个 match 对象 - B —
split()无参数时按任意连续空白分割并自动去除空串 - C —
:b将整数转为二进制,42 的二进制是101010
本章小结
re.search()查第一个匹配,re.findall()查所有匹配,re.sub()做替换- 正则模式用
r"..."原始字符串书写,()定义捕获组 split()/join()/strip()是字符串处理三大高频方法- f-string 支持高级格式说明符:对齐(
>/</^)、精度(.2f)、进制(:b/:x) - 字符串不可变(immutable),所有字符串方法都返回新字符串
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| regular expression | 正则表达式 | 描述文本模式的语法 |
| re module | re 模块 | Python 内置的正则表达式库 |
| split | split | 按分隔符将字符串分割为列表 |
| join | join | 用分隔符将序列合并为字符串 |
| f-string formatting | f-string 格式化 | 使用 {} 和 : 控制输出格式 |
下一步
- 基础阶段复习 → 回顾并巩固基础知识
源码链接
阶段复习:基础部分 (Review Basic)
恭喜你完成了 Python 基础教程的全部 11 个章节。本节将帮助你巩固关键概念、检验学习成果。
知识清单
以下 11 个项目涵盖了基础部分的核心知识点。逐项自检,确保你对每个主题都有清晰理解:
-
变量与表达式 — 能用
=赋值、使用算术运算符(+、-、*、/、%、//)、使用 f-string 格式化 - 基础数据类型 — 能区分 str、int、float、bool、list、dict,并知道它们的基本操作
-
流程控制 — 能使用
if/elif/else分支、三元运算符、match/case模式匹配 -
循环结构 — 能使用
for/while循环、break/continue、enumerate()、zip() -
函数基础 — 能定义函数(
def)、使用参数和返回值、理解*args/**kwargs、lambda 表达式、LEGB 作用域规则 - 列表与字典 — 能使用列表推导式、字典操作(get/items/update)、集合运算、元组解构
-
文件操作 — 能使用
open()读写文件、with上下文管理器、pathlib操作文件系统 -
异常处理 — 能使用
try/except/finally、自定义异常类、raise抛出异常 -
模块与包 — 能使用
import/from...import、理解__name__guard、__all__导出控制 -
面向对象编程 — 能定义类(
class)、使用__init__/self、实现继承和方法重写、理解__str__/__repr__ -
字符串高级处理 — 能使用
re模块正则匹配、常用字符串方法(split/join/strip)、f-string 高级格式
综合练习
练习 1:成绩管理系统
编写一个程序,接收学生姓名和成绩(0-100),存储到字典中。输入 "done" 结束。然后:
- 计算平均分
- 找出最高分和最低分的学生
- 按分数从高到低排序输出
查看答案
scores = {}
while True:
name = input("输入学生姓名 (done 结束): ")
if name == "done":
break
score = int(input(f"输入 {name} 的成绩: "))
scores[name] = score
if scores:
avg = sum(scores.values()) / len(scores)
best = max(scores, key=scores.get)
worst = min(scores, key=scores.get)
print(f"平均分: {avg:.1f}")
print(f"最高分: {best} ({scores[best]})")
print(f"最低分: {worst} ({scores[worst]})")
print("排名:")
for name, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
print(f" {name}: {score}")
练习 2:文件分析器
读取一个文本文件,统计:
- 总行数
- 总词数
- 出现频率最高的 5 个单词
查看答案
from collections import Counter
from pathlib import Path
text = Path("sample.txt").read_text()
lines = text.strip().split("\n")
words = text.lower().split()
print(f"总行数: {len(lines)}")
print(f"总词数: {len(words)}")
top5 = Counter(words).most_common(5)
print("频率 Top 5:")
for word, count in top5:
print(f" {word}: {count}")
练习 3:自定义异常 + 类
创建一个 ValidationError 自定义异常类。再创建一个 User 类,包含 name 和 email 属性——如果 email 不包含 @ 则抛出 ValidationError。
查看答案
class ValidationError(Exception):
def __init__(self, field, value):
super().__init__(f"{field} 验证失败: {value}")
class User:
def __init__(self, name, email):
self.name = name
if "@" not in email:
raise ValidationError("email", email)
self.email = email
def __str__(self):
return f"User({self.name}, {self.email})"
try:
u = User("Alice", "alice@example.com")
print(u)
u2 = User("Bob", "invalid")
except ValidationError as e:
print(f"验证失败: {e}")
自测题库
-
"Python"[1:4]的结果是?- A.
Pyt - B.
yth - C.
Pyth - D.
ytho
- A.
-
以下哪个语句可以遍历字典的键值对?
- A.
for k in d: - B.
for k, v in d.items(): - C.
for v in d.values(): - D.
for i in range(len(d)):
- A.
-
with open("f.txt") as f:中with的作用是?- A. 提高读取速度
- B. 自动关闭文件
- C. 检查文件是否存在
- D. 加密文件内容
-
try...except...else...finally中,else块在什么时候执行?- A. 无论是否异常都执行
- B. 只在发生异常时执行
- C. 只在没有异常时执行
- D. 在 finally 之后执行
-
class Dog(Animal):表示?- A. Dog 是 Animal 的父类
- B. Dog 继承了 Animal
- C. Dog 和 Animal 没有关系
- D. Animal 是一个方法
查看答案
- B — 切片
[1:4]取索引 1,2,3 →yth - B —
.items()返回 (key, value) 元组 - B —
with是上下文管理器,确保文件自动关闭 - C —
else在无异常时执行 - B — 括号内是父类,Dog 继承 Animal
回顾章节链接
遇到不熟悉的主题,可回到对应章节复习:
祝学习顺利!进入进阶部分后,你会接触到异步编程、Web 框架、数据库等更强大的内容。
进阶入门 (Advance Overview)
欢迎进入 Python 进阶教程!在掌握了变量、类型、循环、函数和异常处理等基础知识之后,我们将探索更强大的高级特性,这些特性让你能构建真正的生产级应用。
前提条件:完成 基础部分 的全部 11 个章节。如果你已经熟悉 Python 变量、函数、异常和模块,可以直接从这里开始。
进阶学习路径
| # | 章节 | 难度 | 预计时间 | 描述 |
|---|---|---|---|---|
| 1 | 异步编程 | ⭐⭐⭐ | 25 分钟 | asyncio、async/await、协程、线程池 |
| 2 | FastAPI 路由基础 | ⭐⭐⭐ | 20 分钟 | FastAPI 入门、路由定义、请求处理 |
| 3 | FastAPI 服务器管理 | ⭐⭐⭐ | 25 分钟 | 服务管理、PID 文件、进程控制 |
| 4 | 依赖注入 | ⭐⭐⭐ | 20 分钟 | injector 库、DI 模式、模块绑定 |
| 5 | 数据库操作 | ⭐⭐⭐ | 25 分钟 | PyMySQL、SQLite、参数化查询 |
| 6 | JSON 数据处理 | ⭐⭐ | 15 分钟 | json 模块、自定义序列化、日期处理 |
| 7 | NumPy 数值计算 | ⭐⭐⭐ | 25 分钟 | NumPy 数组、梯度下降算法 |
| 8 | 阶段复习:进阶部分 | — | 30 分钟 | 综合复习与自测 |
[!TIP] 全部 7 个进阶章节预计学习时长约 2.5 小时。每章都配有练习题和自测题。
为什么学这些?
- 异步编程 → 让你的程序在等待 I/O 时不阻塞,大幅提升性能
- FastAPI → Python 中最受欢迎的现代 Web 框架,几行代码就能构建 REST API
- 依赖注入 → 解耦代码、提升可测试性、适用于大型项目
- 数据库 → 与 MySQL 和 SQLite 交互,构建有状态的应用
- JSON → 数据交换的事实标准,前后端通信的桥梁
- NumPy → 科学计算和机器学习的基石
下一步
从 异步编程 开始你的进阶之旅!
异步编程 (Asynchronous Programming)
导语
想象你在开一家咖啡店:如果只有一位服务员,每个客户点单后服务员必须站在柜台等待咖啡做好才能接待下一位,那队伍早就排到街角了。正确的做法是:服务员接单后把订单交给咖啡师,然后立刻去接待下一位客户——咖啡做好后再通知客户取餐。Python 的异步编程正是这个逻辑:让程序在等待 I/O(网络请求、数据库查询、文件读写)时不阻塞,可以去处理其他任务。当你需要并发处理大量网络请求、构建高并发 Web 服务、或编写定时任务调度器时,异步编程能显著提升效率和响应速度。
学习目标
- 理解
async/await语法和协程(coroutine)的工作原理 - 掌握
asyncio.gather()、asyncio.create_task()、asyncio.wait_for()等核心并发模式 - 学会在异步环境中使用
ThreadPoolExecutor处理 CPU 密集型任务
概念介绍
异步编程的核心思想是单线程并发——一个线程通过协作式多任务(cooperative multitasking)同时执行多个操作。Python 通过 asyncio 模块提供支持。
几个关键概念:
- 协程(Coroutine) — 用
async def定义的函数,执行到await时会主动让出控制权,等等待的内容完成后恢复执行。协程是异步编程的基本单元。 - 事件循环(Event Loop) — 异步程序的"调度中心",负责在协程之间切换:当某个协程在等待 I/O 时,事件循环切换到其他就绪的协程继续执行。
await关键字 — 告诉事件循环"这件事我需要等待,你去执行别的任务吧"。只有await后面的操作才是真正"异步"的。- GIL(全局解释器锁) — Python 的 GIL 使得多线程无法真正实现并行计算,但
asyncio是单线程的,不存在 GIL 竞争问题。不过,纯 CPU 密集计算仍会阻塞事件循环,需要配合ThreadPoolExecutor或ProcessPoolExecutor来解决。
[!NOTE]
asyncio特别适合 I/O 密集型场景(网络请求、数据库查询、文件读写),因为等待的时间可以被其他任务利用。对于 CPU 密集型任务,asyncio无法绕过 GIL,需要借助线程池或进程池。
代码示例
示例 1:asyncio.gather() — 并发执行多个任务
import asyncio
async def fetch_data(task_id, delay):
"""模拟一个异步 I/O 操作,例如从网络获取数据。"""
print(f"Task {task_id} started, will take {delay} seconds.")
await asyncio.sleep(delay) # 模拟 I/O 操作的延迟
print(f"Task {task_id} completed.")
return f"Data from task {task_id}"
async def once_main():
# 创建多个异步任务
tasks = [fetch_data(1, 2), fetch_data(2, 3), fetch_data(3, 1)]
# 并发运行所有任务,并等待它们完成
results = await asyncio.gather(*tasks)
# 打印所有任务的返回结果
for result in results:
print(result)
if __name__ == "__main__":
asyncio.run(once_main())
asyncio.gather(*tasks) 并发运行所有协程,并按传入顺序返回结果列表。注意 tasks 中存放的是协程对象(调用 fetch_data() 但未 await),而 gather 会并发调度它们。三个任务总耗时等于最长的单个任务耗时(3秒),而非累加(6秒)。
[!TIP]
gather的特点是「等所有人到齐再出发」——它会在所有任务完成后一次性返回结果列表。如果某个任务抛出异常,异常会传播给gather的调用者。
示例 2:asyncio.create_task() + asyncio.wait_for() — 任务管理与超时控制
import asyncio
async def fetch_data(task_id, delay):
"""模拟一个异步 I/O 操作。"""
print(f"Task {task_id} started, will take {delay} seconds.")
await asyncio.sleep(delay)
print(f"Task {task_id} completed.")
return f"Data from task {task_id}"
async def task_main():
# 使用 create_task 创建独立的调度任务
tasks = [
asyncio.create_task(fetch_data(1, 2)),
asyncio.create_task(fetch_data(2, 4)),
asyncio.create_task(fetch_data(3, 1)),
]
try:
# 设置一个超时时间,假设我们希望所有任务在3秒内完成
results = await asyncio.wait_for(asyncio.gather(*tasks), timeout=3)
except asyncio.TimeoutError:
print("Some tasks took too long and were cancelled.")
# 处理任务结果
for task in tasks:
if not task.cancelled():
try:
result = task.result()
print(f"Task result: {result}")
except Exception as e:
print(f"Task raised an exception: {e}")
if __name__ == "__main__":
asyncio.run(task_main())
asyncio.create_task() 将协程封装为 Task 对象并立即调度。与直接传入协程的区别在于:Task 是独立调度的实体,可以在外部控制它的生命周期(取消、检查状态等)。asyncio.wait_for(coro, timeout=N) 为整个操作设置超时边界,超时后抛出 asyncio.TimeoutError。
[!WARNING]
wait_for超时会取消内部的所有任务,但Task对象本身仍存在。处理结果前务必用task.cancelled()检查取消状态,否则访问task.result()会抛出CancelledError。
示例 3:ThreadPoolExecutor — 在异步中处理 CPU 密集型任务
import asyncio
from concurrent.futures import ThreadPoolExecutor
async def cpu_bound_task():
"""模拟 CPU 密集型任务。"""
total = 0
for i in range(10000000):
total += i
print(f"CPU task finished, sum: {total}")
async def io_bound_task():
"""模拟 I/O 密集型任务。"""
await asyncio.sleep(1)
print("I/O task finished")
async def thread_pool_task():
with ThreadPoolExecutor() as executor:
loop = asyncio.get_running_loop()
# 将 CPU 密集型任务提交到线程池,避免阻塞事件循环
future = loop.run_in_executor(executor, cpu_bound_task)
# 同时执行 I/O 密集型任务
await io_bound_task()
# 等待 CPU 任务完成
await future
def thread_main():
asyncio.run(thread_pool_task())
if __name__ == "__main__":
thread_main()
loop.run_in_executor() 把阻塞操作放到线程池中执行,事件循环不会被卡住。I/O 任务和 CPU 任务在此可以并行运行——io_bound_task 不会等待 cpu_bound_task 完成。
[!NOTE] 为什么用线程池而不是进程池?因为
run_in_executor默认使用线程池。对于 Python,由于 GIL 的存在,线程池对 CPU 密集型的加速效果有限。如果确实需要 CPU 并行,可以改用ProcessPoolExecutor:loop.run_in_executor(ProcessPoolExecutor(), func)。
常见错误与解决
[!WARNING] 错误 1:在异步代码中使用阻塞 I/O
import asyncio import time # 💥 阻塞式睡眠! async def bad_example(): time.sleep(2) # 阻塞整个事件循环,其他协程全部卡住 print("done") asyncio.run(bad_example())原因:
time.sleep()是同步阻塞的,会冻结事件循环,所有其他协程都无法执行。解决:使用异步替代方案。
async def good_example(): await asyncio.sleep(2) # ✅ 让出控制权,事件循环继续调度其他协程 print("done")
[!WARNING] 错误 2:忘记
await协程async def fetch(): return "data" async def main(): result = fetch() # 💥 没有 await!返回的是协程对象,而非 "data" print(result) # <coroutine object fetch at 0x...> asyncio.run(main())原因:调用
async def函数不会立即执行函数体,而是返回一个协程对象。必须用await才能真正执行。解决:对每个协程都要
await。async def main(): result = await fetch() # ✅ 正确 print(result) # data
最佳实践
-
永远用
await而非阻塞调用 —time.sleep()→asyncio.sleep(),requests.get()→httpx.get()或aiohttp,同步数据库驱动 → async 驱动(如aiomysql) -
合理选择并发原语 —
gather()适合"收集所有结果",create_task()适合"后台调度",wait_for()适合"设置超时",as_completed()适合"谁先完成先处理谁"
练习
- 编写一个异步爬虫函数
fetch_urls(urls),并发获取多个 URL 的内容(可用asyncio.sleep模拟),并打印每个 URL 的结果。
查看答案
import asyncio
async def fetch_url(url):
"""模拟网络请求。"""
await asyncio.sleep(1) # 模拟延迟
return f"Content of {url}"
async def fetch_urls(urls):
tasks = [fetch_url(url) for url in urls]
results = await asyncio.gather(*tasks)
for url, content in zip(urls, results):
print(f"{url}: {content}")
asyncio.run(fetch_urls([
"https://example.com/api/users",
"https://example.com/api/posts",
"https://example.com/api/comments",
]))
- 实现一个带超时的异步函数
safe_fetch(task_id, delay, timeout),如果任务超时则返回"Timeout",否则返回实际结果。
查看答案
import asyncio
async def safe_fetch(task_id, delay, timeout):
try:
await asyncio.sleep(delay)
return f"Data from task {task_id}"
except asyncio.TimeoutError:
return "Timeout"
async def main():
# 方式一:在外部用 wait_for 控制
result = await asyncio.wait_for(
safe_fetch(1, 2, 3), timeout=3
)
print(result) # Data from task 1
# 方式二:内部处理超时
result = await asyncio.wait_for(
safe_fetch(2, 5, 3), timeout=3
)
print(result)
asyncio.run(main())
知识检查
-
以下哪个函数是
asyncio中正确的非阻塞延迟调用?- A.
time.sleep(1) - B.
asyncio.sleep(1) - C.
await sleep(1) - D.
async sleep(1)
- A.
-
asyncio.gather()的返回值顺序与什么有关?- A. 任务完成的先后顺序
- B. 传入任务的顺序
- C. 随机顺序
- D. 按任务耗时排序
-
在 Python 异步编程中,GIL 对以下哪种场景影响最大?
- A. 大量并发网络请求(I/O 密集型)
- B. 大量并发文件读写(I/O 密集型)
- C. 大规模数值计算(CPU 密集型)
- D. 定时器调度(CPU 密集型)
查看答案
- B —
asyncio.sleep(1)是协程,必须用await调用 - B —
gather()按传入顺序返回结果,与完成时间无关 - C — GIL 限制多线程/单线程中的 CPU 并行计算,I/O 密集型不受影响
本章小结
async/await是 Python 异步编程的核心语法,async def定义协程,await挂起等待asyncio.gather()并发执行多个协程并按传入顺序返回结果asyncio.create_task()创建独立调度任务,asyncio.wait_for()为任务设置超时- I/O 密集型场景用
asyncio效果最好,CPU 密集型任务需配合ThreadPoolExecutor或ProcessPoolExecutor - 永远不要在
async def中使用阻塞调用(如time.sleep()),要用异步替代方案
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| coroutine | 协程 | 用 async def 定义的异步函数,可被 await 挂起和恢复 |
| await | 等待关键字 | 挂起当前协程,等待异步操作完成后恢复 |
| event loop | 事件循环 | 异步程序的调度中心,负责在协程之间切换执行 |
| asyncio | 异步 I/O 模块 | Python 标准库中的异步编程框架,提供事件循环和协程工具 |
| GIL | 全局解释器锁 | Python 解释器级别的锁,限制多线程并行执行字节码 |
下一步
- FastAPI 路由与请求处理 → 将异步编程应用到 Web 开发
源码链接
FastAPI 路由基础 (FastAPI Routes)
导语
你每天使用的各种 App 都在通过 API 与服务器交换数据:发一条消息、搜索商品、下单支付——背后都是 HTTP 接口在运作。FastAPI 是 Python 生态中最高效的 Web 框架之一,它让你用几行代码就能写出高性能的 REST API。本节带你迈出 Web 开发的第一步。
学习目标
- 了解 FastAPI 框架的基本概念和使用场景
- 学会定义路由(
@app.get())和返回 JSON 数据 - 掌握使用 uvicorn 启动开发服务器
概念介绍
FastAPI 是一个现代 Python Web 框架,基于 Starlette 和 Pydantic 构建。它的特点包括:
- 自动文档:自动生成交互式 Swagger UI 和 ReDoc
- 类型提示:利用 Python 类型注解实现请求/响应验证
- 异步支持:原生 async/await
- 高性能:接近 Node.js 和 Go 的水平
[!NOTE] 本节使用
fastapi_sample.py(125 行)展示基本路由定义。fastapi_server_sample.py(332 行)将在下一章节讲解更高级的服务器管理。
代码示例
示例 1:基本应用定义
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello():
return {"message": "Hello, FastAPI!"}
FastAPI() 实例化应用,@app.get("/") 是一个装饰器,将 hello() 函数注册为处理 GET / 路由的请求处理器。返回的字典会自动序列化为 JSON。
示例 2:启动服务器
import uvicorn
# 启动开发服务器
uvicorn.run(app, host="0.0.0.0", port=8000)
uvicorn 是 ASGI 服务器,负责接收 HTTP 请求并转发给 FastAPI 应用。host="0.0.0.0" 允许外部访问。
示例 3:带参数的路由
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id, "name": f"User {user_id}"}
@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
路径参数通过 {param_name} 定义,查询参数通过函数参数定义。FastAPI 会自动进行类型验证。
[!TIP] 启动服务器后访问
http://localhost:8000/docs可以看到自动生成的交互式 API 文档。
常见错误与解决
[!WARNING] 错误 1:忘记启动 ASGI 服务器
直接运行
python app.py不会启动服务器——FastAPI 本身不包含 Web 服务器。解决:必须使用
uvicorn.run(app)或在命令行运行uvicorn app:app。
[!WARNING] 错误 2:路径参数类型不匹配
访问
/users/abc会返回 422 验证错误——user_id声明为int类型。解决:这正是 FastAPI 的优势——自动类型验证阻止了无效请求。
最佳实践
- 始终使用 async def 对于 I/O 密集型路由
- 利用类型提示 — FastAPI 用它来生成文档和验证请求
- 先写好模型 — 用 Pydantic BaseModel 定义请求/响应数据结构
练习
- 定义一个路由
GET /greet/{name},返回{"greeting": "Hello, <name>!"}.
查看答案
@app.get("/greet/{name}")
async def greet(name: str):
return {"greeting": f"Hello, {name}!"}
- 定义一个路由
GET /search,接受查询参数q(字符串,默认值为None),返回搜索结果的占位符。
查看答案
@app.get("/search")
async def search(q: str = None):
if q:
return {"query": q, "results": []}
return {"error": "Please provide a search query"}
知识检查
-
FastAPI 自动生成的 API 文档可以通过哪个路径访问?
- A.
/api - B.
/docs - C.
/swagger - D.
/admin
- A.
-
@app.post("/users")路由可以处理哪种 HTTP 请求?- A. GET
- B. POST
- C. PUT
- D. DELETE
-
FastAPI 的路径参数
{user_id}支持类型注解吗?- A. 不支持,所有路径参数都是字符串
- B. 支持,可以在函数参数上标注类型
- C. 只有整数类型支持
- D. 只支持可选参数
查看答案
- B — FastAPI 自动生成
/docs(Swagger UI) 和/redoc - B —
@app.post()处理 POST 请求 - B — FastAPI 支持所有 Python 类型注解进行自动验证
本章小结
- FastAPI 是基于 ASGI 的现代 Python Web 框架
@app.get()等装饰器注册路由处理器- 返回值字典自动序列化为 JSON
- uvicorn 是推荐的生产级 ASGI 服务器
- FastAPI 自动生成交互式 API 文档(/docs)
- 路径参数和查询参数都支持类型注解和自动验证
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| route | 路由 | HTTP 路径与处理函数的映射 |
| decorator | 装饰器 | Python 修饰函数的语法特性 |
| ASGI | ASGI | 异步服务器网关接口 |
| endpoint | 端点 | API 的具体 URL 路径 |
| serialization | 序列化 | Python 对象转换为 JSON 格式 |
下一步
- FastAPI 服务器管理 → 学会管理服务进程、PID 和日志
源码链接
FastAPI 服务器管理 (FastAPI Server)
导语
在生产环境中,一个 Web 应用不仅要能启动和运行,还要能优雅地处理信号、管理进程、记录日志、支持热重载和关闭。FastAPI 的基础路由让你能定义 API,但如何像真正的服务一样管理它——启动、停止、重启、PID 文件管理——这才是生产级开发的关键技能。
学习目标
- 学会使用 ServiceManager 模式管理 FastAPI 服务
- 掌握 PID 文件管理和进程信号处理
- 了解 uvicorn 的生产配置和后台运行
概念介绍
fastapi_server_sample.py(332 行)展示了一个完整的生产级服务器管理模式:
- ServiceManager 类:封装了 FastAPI 应用的生命周期管理
- PID 文件:记录进程 ID,用于后续的查询和终止
- 信号处理:响应
SIGTERM/SIGINT优雅关闭 - 日志管理:将 stdout/stderr 重定向到日志文件
这种模式在微服务架构中很常见——服务需要可管理、可监控、可重启。
代码示例
示例 1:ServiceManager 类
class ServiceManager:
"""管理 FastAPI/Uvicorn 服务的类。"""
def __init__(self, app_name, config):
self.app_name = app_name
self.config = config
self.pid_file = f"{app_name}.pid"
self.log_file = f"{app_name}.log"
def start(self):
"""启动服务并记录 PID"""
# ... uvicorn.run() 配置
pid = os.getpid()
with open(self.pid_file, "w") as f:
f.write(str(pid))
print(f"Started {self.app_name} with PID {pid}")
示例 2:进程管理
def stop(self):
"""通过读取 PID 文件终止服务"""
pid = self._read_pid()
if pid:
os.kill(pid, signal.SIGTERM)
self._remove_pid()
print(f"Stopped {self.app_name} (PID {pid})")
def _read_pid(self):
"""读取保存的 PID"""
if os.path.exists(self.pid_file):
with open(self.pid_file) as f:
return int(f.read().strip())
return None
示例 3:信号处理
import signal
import sys
def _signal_handler(self, signum, frame):
print(f"Received signal {signum}")
self._cleanup()
sys.exit(0)
def start(self):
# 注册信号处理器
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGINT, self._signal_handler)
# 启动 uvicorn...
[!NOTE] 信号处理确保服务器接收到
kill命令时能优雅关闭连接,不会丢失正在处理的请求。
常见错误与解决
[!WARNING] 错误 1:PID 文件残留导致无法启动
服务器异常退出后 PID 文件还存在,下次启动时检查到"服务正在运行"。
解决:启动时先检查 PID 文件中的进程是否真的存在(
os.kill(pid, 0)测试)。
[!WARNING] 错误 2:
uvicorn.run()阻塞主线程
uvicorn.run()是阻塞调用,会阻塞主线程,导致后续代码无法执行。解决:使用
target参数 + 线程,或用subprocess在子进程中启动。
最佳实践
- 始终使用 PID 文件 管理服务进程生命周期
- 注册信号处理 确保优雅关闭(graceful shutdown)
- 日志输出到文件 而非 stdout(生产环境中 stdout 不可靠)
练习
- 写一个函数,检查 PID 文件中保存的进程是否真的存在。
查看答案
import os
def is_running(pid):
try:
os.kill(pid, 0)
return True
except ProcessLookupError:
return False
except PermissionError:
return True
pid = int(open("service.pid").read())
print(f"Service running: {is_running(pid)}")
- 用
subprocess启动一个 uvicorn 服务器(不阻塞主进程)。
查看答案
import subprocess
# 非阻塞启动
process = subprocess.Popen(
["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"],
stdout=open("server.log", "a"),
stderr=subprocess.STDOUT,
)
print(f"Server started with PID: {process.pid}")
知识检查
-
PID 文件的主要作用是?
- A. 记录服务器日志
- B. 记录进程 ID 用于后续管理
- C. 配置服务器端口
- D. 存储环境变量
-
signal.SIGTERM信号的含义是?- A. 强制终止进程(无法捕获)
- B. 请求终止进程(可捕获和处理)
- C. 暂停进程
- D. 恢复进程
-
os.kill(pid, 0)的作用是?- A. 终止指定的进程
- B. 向进程发送空信号
- C. 检查进程是否存在,不实际发送信号
- D. 修改进程优先级
查看答案
- B — PID 文件记录进程 ID,用于 stop/restart 操作
- B — SIGTERM 是可捕获的终止请求(
kill默认发送) - C —
signal 0是一个特殊的"检查存在"信号
本章小结
- ServiceManager 封装了服务的完整生命周期
- PID 文件是管理服务进程的关键工具
- 信号处理(SIGTERM/SIGINT)确保优雅关闭
os.kill(pid, 0)可以检查进程是否存在uvicorn.run()是阻塞调用,需要线程/subprocess 管理- 日志输出应重定向到文件
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| daemon | 守护进程 | 在后台持续运行提供服务 |
| PID file | PID 文件 | 记录进程 ID 的文件 |
| signal handling | 信号处理 | 捕获和处理操作系统信号 |
| graceful shutdown | 优雅关闭 | 在完成当前请求后关闭 |
| ASGI server | ASGI 服务器 | 运行 ASGI 应用的服务器 |
下一步
- 依赖注入 → 学会使用 injector 管理依赖关系
源码链接
依赖注入 (Dependency Injection)
导语
大型 Python 项目中,对象之间的依赖关系错综复杂:用户服务需要数据库连接,数据库连接需要配置,配置需要环境变量。手动管理这些依赖不仅繁琐,而且难以测试。依赖注入(DI)是一种将"创建依赖"与"使用依赖"解耦的模式——让组件只关注自己的职责,依赖由外部容器提供。本节带你使用 injector 库体验 Python 中的依赖注入。
学习目标
- 了解依赖注入(DI)的基本概念和优势
- 学会使用
injector库定义模块和绑定 - 掌握
@inject装饰器和@provider方法的使用
概念介绍
依赖注入的核心思想是:一个对象不需要知道如何创建它依赖的对象——它只需要声明需要什么,由外部容器(Injector)负责创建和注入。
injector 库提供了三个关键概念:
- Module — 定义一组绑定(什么类型映射到什么实现)
@inject— 标注构造函数参数需要注入@provider— 方法返回一个实例,Injector 会自动调用
这种模式在大型应用中尤其有用——单元测试可以替换依赖为 mock 对象。
代码示例
示例 1:基本类型绑定
from injector import Binder, Injector, Module, inject
from typing import NewType
Name = NewType("Name", str)
Description = NewType("Description", str)
class User:
@inject
def __init__(self, name: Name, description: Description):
self.name = name
self.description = Description
class UserModule(Module):
def configure(self, binder: Binder):
binder.bind(User)
class UserAttributeModule(Module):
def configure(self, binder: Binder):
binder.bind(Name, to="Sherlock")
@provider
def describe(self, name: Name) -> Description:
return f"{name} is a man of astounding insight"
# 创建 Injector 并注入依赖
injector = Injector([UserModule(), UserAttributeModule()])
user = injector.get(User)
print(f"User: {user.name}")
print(f"Description: {user.description}")
示例 2:Provider 方法
@provider
def describe(self, name: Name) -> Description:
return f"{name} is a man of astounding insight"
@provider 装饰器告诉 Injector:当需要 Description 类型时,调用这个方法。Injector 会自动解析 describe 的参数 name: Name。
示例 3:获取注入实例
injector = Injector([UserModule(), UserAttributeModule()])
user = injector.get(User)
injector.get(User) 触发整个依赖图:Injector 查看 User 的 @inject 标注的构造函数,发现需要 Name 和 Description,然后查找对应模块的绑定和 provider 方法。
[!NOTE]
NewType创建了一个类型别名——这在 DI 中很实用:Name = NewType("Name", str)让 Injector 区分Name和普通的str。
常见错误与解决
[!WARNING] 错误 1:忘记
@inject装饰器构造函数参数没有
@inject,Injector 不知道哪些参数需要注入。解决:所有需要注入依赖的构造函数必须用
@inject装饰。
[!WARNING] 错误 2:绑定循环
A 依赖 B,B 依赖 A——Injector 无法解析循环依赖。
解决:重新设计依赖关系,或使用延迟注入(Lazy Injection)。
最佳实践
- 使用 NewType 创建类型标识 — 避免同类型不同含义的冲突
- 分模块组织绑定 — 相关依赖放在同一个 Module 类中
- 优先构造函数注入 — 避免属性注入导致的隐式依赖
练习
- 定义一个
APIUrl = NewType("APIUrl", str)类型,绑定到"https://api.example.com",注入到一个APIClient类中。
查看答案
from injector import Binder, Injector, Module, inject
from typing import NewType
APIUrl = NewType("APIUrl", str)
class APIClient:
@inject
def __init__(self, url: APIUrl):
self.url = url
class ConfigModule(Module):
def configure(self, binder: Binder):
binder.bind(APIUrl, to="https://api.example.com")
binder.bind(APIClient)
injector = Injector([ConfigModule()])
client = injector.get(APIClient)
print(f"API URL: {client.url}")
- 用
@provider方法创建一个DatabaseConnection实例,依赖APIUrl。
查看答案
class DatabaseModule(Module):
@provider
def db_connection(self, url: APIUrl) -> DatabaseConnection:
return DatabaseConnection(url)
知识检查
-
@inject装饰器的作用是:- A. 标记函数为异步
- B. 标注构造函数参数需要依赖注入
- C. 自动记录函数调用日志
- D. 创建类的单例实例
-
@provider方法返回的值会被 Injector 如何处理?- A. 忽略
- B. 缓存并注入到依赖该类型的对象中
- C. 仅当第一次调用时缓存,后续重新计算
- D. 直接丢弃
-
NewType("Name", str)的用途是?- A. 创建 str 的子类
- B. 在类型层面区分同名但不同含义的类型
- C. 提升运行时性能
- D. 替代 dataclass
查看答案
- B —
@inject告诉 Injector 需要注入构造函数参数 - B — provider 返回值被缓存并在依赖图中分发
- B — NewType 创建一个新类型,仅在类型检查时区分,运行时等价于原类型
本章小结
- 依赖注入将"创建依赖"与"使用依赖"解耦
injector库提供 Module、@inject、@provider三件套- Module 定义绑定,Injector 解析依赖图并注入
- NewType 是创建类型标识的好方法
- DI 使单元测试更容易——可以替换依赖为 mock 对象
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| dependency injection | 依赖注入 | 将依赖从外部注入对象,而非内部创建 |
| module | 模块 | 一组依赖绑定的集合 |
| binder | 绑定器 | 负责注册类型映射 |
| provider | 提供者 | 返回特定类型实例的方法 |
| NewType | 新类型 | Python 类型别名机制,用于类型检查 |
下一步
- 数据库操作 → 学会使用 PyMySQL 和 SQLite 操作数据库
源码链接
数据库操作 (Database Operations)
导语
在现代 Web 应用中,数据几乎总是持久化存储在数据库中。无论是电商平台的商品库存、社交网络的用户关系,还是博客系统的文章档案,都离不开数据库的支撑。Python 提供了多种数据库交互方式:标准库内置的 sqlite3 适合轻量级场景和开发测试,而 PyMySQL 等第三方库则用于连接 MySQL 等生产级数据库。掌握数据库操作,意味着你的程序不再只是"一次性脚本",而是能够存储、查询和管理真实数据的应用。
学习目标
- 掌握 SQLite 内存数据库的基本操作(建表、插入、查询、更新、删除)
- 理解 PyMySQL 连接 MySQL 数据库的完整流程与错误处理
- 学会使用参数化查询防止 SQL 注入攻击
概念介绍
数据库是结构化存储数据的系统。Python 与数据库交互的核心模式可以用"连接 → 游标 → 执行 → 获取"四个步骤概括:
- 连接(Connection) — 建立程序与数据库之间的通信通道。每个数据库驱动(如
sqlite3、pymysql)都有自己的连接函数,需要传入连接参数(主机、端口、用户名、密码、数据库名)。 - 游标(Cursor) — 连接创建后,通过
cursor()获取游标对象。游标是执行 SQL 语句的"句柄",所有 SQL 操作都通过游标的execute()方法完成。 - 执行(Execute) — 游标的
execute()方法接受 SQL 字符串和参数元组,发送给数据库执行。对于修改数据的操作(INSERT/UPDATE/DELETE),还需要调用连接的commit()来提交事务。 - 获取(Fetch) — 对于查询操作(SELECT),用
fetchone()获取一行、fetchmany(n)获取 n 行、fetchall()获取所有结果。
[!NOTE] SQLite vs MySQL:SQLite 是嵌入式数据库,数据存储在单一文件中(或内存中),不需要额外服务器,非常适合原型开发和测试。MySQL 是独立的数据库服务器,支持多用户并发访问,是生产环境的常见选择。
代码示例
示例 1:SQLite 内存数据库 — 完整的 CRUD 操作
import sqlite3
# 使用内存数据库(不需要磁盘文件,适合学习和测试)
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
# 创建表
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
salary REAL
)
""")
# 插入数据(使用参数化查询 ? 占位符)
cursor.execute(
"INSERT INTO employees (name, department, salary) VALUES (?, ?, ?)",
("John Doe", "IT", 75000.00),
)
cursor.execute(
"INSERT INTO employees (name, department, salary) VALUES (?, ?, ?)",
("Jane Smith", "HR", 65000.00),
)
conn.commit() # 提交事务
# 查询全部
cursor.execute("SELECT * FROM employees")
for row in cursor.fetchall():
print(f"ID: {row[0]}, Name: {row[1]}, Dept: {row[2]}, Salary: {row[3]}")
# 条件查询
cursor.execute("SELECT name, salary FROM employees WHERE salary > ?", (70000,))
for row in cursor.fetchall():
print(f"Name: {row[0]}, Salary: {row[1]}")
conn.close()
SQLite 使用 ? 作为参数占位符。fetchall() 返回元组列表,每个元组代表一行数据,通过索引 row[0]、row[1] 访问列值。记住修改数据后调用 conn.commit(),否则更改不会生效。
[!TIP] 使用
with sqlite3.connect(...) as conn:上下文管理器可以自动处理事务提交和回滚,代码更简洁安全。
示例 2:PyMySQL 连接 MySQL 数据库
import pymysql
DB_CONFIG = {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "your_password",
"database": "test_db",
}
def connect_sample():
"""连接 MySQL 并获取数据库版本信息。"""
try:
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
cursor.execute("SELECT VERSION()")
data = cursor.fetchone()
print(f"Database version: {data}")
cursor.close()
db.close()
except pymysql.Error as e:
print(f"MySQL connection failed: {e}")
# 查询示例
def query_sample():
"""执行基本 SQL 操作。"""
try:
db = pymysql.connect(**DB_CONFIG)
cursor = db.cursor()
cursor.execute("SELECT * FROM demo")
rows = cursor.fetchall()
for row in rows:
print(f"row: {row}")
cursor.close()
db.close()
except pymysql.Error as e:
print(f"Query failed: {e}")
PyMySQL 使用方式与 sqlite3 类似:连接 → 游标 → 执行 → 获取。主要区别在于 PyMySQL 需要配置服务器连接参数(host、port、user 等),并且异常类型是 pymysql.Error。生产环境中应始终使用 try-except 包裹数据库操作。
[!WARNING] 生产环境中不要把数据库密码硬编码在代码里!应该使用环境变量或配置文件:
os.environ.get("DB_PASSWORD")。
示例 3:参数化查询 — 防止 SQL 注入
import sqlite3
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
# ❌ 危险的拼接方式
user_input = "Alice'; DROP TABLE users; --"
# cursor.execute(f"INSERT INTO users (name) VALUES ('{user_input}')") # 会删除整张表!
# ✅ 安全的参数化查询
cursor.execute("INSERT INTO users (name) VALUES (?)", (user_input,))
conn.commit()
cursor.execute("SELECT * FROM users")
print(cursor.fetchall()) # [('Alice\'; DROP TABLE users; --',)] — 安全地作为数据存储
conn.close()
永远不要用 Python 字符串拼接(f-string、%、+)构建 SQL 语句。参数化查询将 SQL 模板和数据分开传输给数据库,数据库引擎会将参数值视为纯数据而非 SQL 命令的一部分,从根本上杜绝 SQL 注入。
[!WARNING] SQL 注入攻击是最常见的 Web 安全漏洞之一。攻击者通过在输入中注入恶意 SQL 片段,可以读取、修改或删除数据库中的任何数据。参数化查询是唯一可靠的防御方式。
常见错误与解决
[!WARNING] 错误 1:忘记
commit()导致数据丢失cursor.execute("INSERT INTO users (name) VALUES ('Alice')") # 忘记调用 conn.commit() cursor.execute("SELECT * FROM users") print(cursor.fetchall()) # [] — 查不到刚插入的数据!原因:SQLite 和 PyMySQL 默认开启事务模式,所有修改操作(INSERT/UPDATE/DELETE)都需要显式
commit()才能持久化。解决:在每个修改操作后调用
conn.commit(),或使用上下文管理器with语句。
[!WARNING] 错误 2:单参数元组忘记加逗号
cursor.execute("SELECT * FROM users WHERE id = ?", (1)) # 💥 (1) 是整数 1,不是元组! # 应该写成: cursor.execute("SELECT * FROM users WHERE id = ?", (1,)) # ✅ 注意末尾的逗号原因:Python 中
(1)等价于1(括号只是数学分组),(1,)才是包含一个元素的元组。解决:单个参数时务必在末尾加逗号。
最佳实践
- 始终使用参数化查询 — 所有用户输入、外部数据都通过
?或%s占位符传入,绝不用字符串拼接构建 SQL - 使用上下文管理器(
with) —with sqlite3.connect(...) as conn:自动处理提交/回滚和连接关闭,避免资源泄漏
练习
- 使用 SQLite 创建一个
books表(字段:id、title、author、year),插入至少 3 本书,然后查询所有 2000 年以后出版的书籍。
查看答案
import sqlite3
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT,
year INTEGER
)
""")
books = [
("Python 编程从入门到精通", "张三", 2018),
("流畅的Python", "Luciano Ramalho", 2016),
("Effective Python", "Brett Slatkin", 2019),
]
for title, author, year in books:
cursor.execute(
"INSERT INTO books (title, author, year) VALUES (?, ?, ?)",
(title, author, year),
)
conn.commit()
cursor.execute("SELECT * FROM books WHERE year > ?", (2000,))
for row in cursor.fetchall():
print(row)
conn.close()
- 编写一个函数
search_user(cursor, name),使用参数化查询安全地搜索用户名中包含指定关键字的用户。
查看答案
def search_user(cursor, name):
"""使用 LIKE 进行模糊查询,参数化防止注入。"""
cursor.execute("SELECT * FROM users WHERE name LIKE ?", (f"%{name}%",))
return cursor.fetchall()
# 使用示例
# results = search_user(cursor, "Al") # 查找名字中包含 "Al" 的用户
知识检查
-
SQLite 中
fetchone()的返回值类型是?- A. 列表
- B. 字典
- C. 元组
- D. 字符串
-
防止 SQL 注入的正确方式是?
- A. 使用字符串格式化(f-string)
- B. 对用户输入进行 HTML 转义
- C. 使用参数化查询(占位符)
- D. 限制输入长度
-
以下哪个 SQL 操作不需要
commit()?- A.
INSERT - B.
UPDATE - C.
DELETE - D.
SELECT
- A.
查看答案
- C —
fetchone()返回单个元组,代表一行数据 - C — 参数化查询将 SQL 和数据分开传输,是防御 SQL 注入的唯一可靠方式
- D —
SELECT是只读操作,不会修改数据,不需要 commit
本章小结
- 数据库操作遵循"连接 → 游标 → 执行 → 获取"的标准模式
- SQLite 无需服务器,适合学习和原型开发;MySQL 适合生产环境
- 修改数据后必须
commit()才能持久化 - 参数化查询(
?或%s占位符)是防止 SQL 注入的唯一可靠方式 - 使用上下文管理器
with可以自动管理事务和连接资源
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| database | 数据库 | 结构化存储和检索数据的系统 |
| connection | 数据库连接 | 程序与数据库之间的通信通道 |
| cursor | 游标 | 用于执行 SQL 语句和获取结果的对象 |
| parameterized query | 参数化查询 | 使用占位符传递参数,而非字符串拼接的查询方式 |
| SQL injection | SQL 注入 | 通过构造恶意输入篡改 SQL 语句的攻击方式 |
下一步
- JSON 数据处理 → 学习如何在 Python 中序列化/反序列化 JSON 数据
源码链接
JSON 数据处理 (JSON Data Processing)
导语
在互联网世界中,数据交换几乎都使用 JSON 格式。当你的前端页面通过 API 从后端获取用户信息时,当移动应用与服务器同步数据时,当配置文件被读取时——背后都是 JSON 在承载数据。JSON(JavaScript Object Notation)源自 JavaScript 的对象字面量语法,但因简洁、可读、语言无关,已成为跨语言数据传输的通用标准。Python 内置的 json 模块提供了完善的序列化和反序列化工具,让你轻松在 Python 对象与 JSON 文本之间来回转换。
学习目标
- 掌握
json.dumps()和json.loads()的基本用法,包括中文支持和格式化输出 - 学会自定义 JSON 编码器处理 datetime 和自定义类对象
- 掌握 JSON 文件的读写操作
概念介绍
JSON 本质上是结构化的文本格式,它支持的数据类型有限但实用:字符串、数字、布尔值、null、对象(字典)和数组(列表)。Python 的 json 模块提供了四组核心函数:
json.dumps()(序列化) — 将 Python 对象转换为 JSON 字符串。dump中的s代表string。常用参数:indent(缩进空格数,美化输出)、ensure_ascii=False(允许输出中文字符)、cls(自定义编码器类)。json.loads()(反序列化) — 将 JSON 字符串解析为 Python 对象。loads中的s代表string。json.dump()— 将 Python 对象直接写入文件对象(File object),而非返回字符串。json.load()— 从文件对象读取并解析 JSON 数据。
[!NOTE] JSON 与 Python 数据类型的对应关系:
object↔dict,array↔list,string↔str,number↔int/float,true/false↔True/False,null↔None。注意 Python 的True/False/None与 JSON 的true/false/null大小写不同。
代码示例
示例 1:基本序列化与反序列化
import json
data = {
"name": "辽宁产串红小番茄",
"price": 12.8,
"in_stock": True,
"varieties": ["串红", "樱桃番茄", "黄珍珠"],
"weight_grams": None,
"harvest_date": "2023-10-15",
}
# 序列化为 JSON 字符串
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
# 输出:
# {
# "name": "辽宁产串红小番茄",
# "price": 12.8,
# ...
# }
# 从 JSON 字符串反序列化
parsed_data = json.loads(json_str)
print(parsed_data["name"]) # 辽宁产串红小番茄
ensure_ascii=False 是关键参数——默认情况下 dumps() 会将非 ASCII 字符转义为 \uXXXX 形式,设置 False 后中文直接输出,可读性更好。indent=2 使输出具有 2 空格缩进,便于人类阅读(调试时推荐,生产环境为节省带宽通常省略)。
示例 2:自定义编码器 — 处理 datetime 和自定义对象
import json
from datetime import datetime
class Product:
def __init__(self, name: str, expiry_date: datetime):
self.name = name
self.expiry_date = expiry_date
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, Product):
return {"name": obj.name, "expiry_date": obj.expiry_date.isoformat()}
return super().default(obj)
tomato = Product("辽宁串红番茄", datetime(2023, 12, 31))
product_list = {"product": tomato, "update_time": datetime.now()}
json_with_date = json.dumps(product_list, cls=CustomEncoder, indent=2)
print(json_with_date)
# 输出:
# {
# "product": {
# "name": "辽宁串红番茄",
# "expiry_date": "2023-12-31T00:00:00"
# },
# "update_time": "2026-04-27T10:30:00.123456"
# }
json 模块默认只支持基本类型。遇到 datetime、自定义类等无法直接序列化的对象时会抛出 TypeError。通过继承 json.JSONEncoder 并重写 default() 方法,可以指定自定义类型的序列化规则。default() 方法在遇到未知类型时被调用,返回可序列化的 Python 基本类型。
[!TIP] 如果只需临时处理一两个自定义类型,也可以用
default参数而非定义完整类:json.dumps(obj, default=lambda o: o.__dict__)。
示例 3:JSON 文件读写
import json
data = {
"name": "辽宁产串红小番茄",
"price": 12.8,
"in_stock": True,
}
# 写入 JSON 文件
with open("data/tomato.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 从 JSON 文件读取
with open("data/tomato.json", "r", encoding="utf-8") as f:
file_data = json.load(f)
print(file_data) # {'name': '辽宁产串红小番茄', 'price': 12.8, 'in_stock': True}
注意 dump()/load() 与 dumps()/loads() 的区别:不带 s 的版本操作文件对象(File object),带 s 的版本操作字符串。两种 API 的参数基本相同。文件操作时务必指定 encoding="utf-8",否则在某些系统上可能产生编码问题。
[!NOTE] 对于追求极致性能的场景,可以考虑
ujson(UltraJSON)库——它是用 C 实现的超fast JSON 编解码器,API 与标准库json模块完全兼容,但速度可提升数倍。
常见错误与解决
[!WARNING] 错误 1:未设置
ensure_ascii=False导致中文乱码import json data = {"city": "大连"} print(json.dumps(data))
输出: {"city": "\u5927\u8fde"} — 中文变成了 Unicode 转义序列
**原因**:`json.dumps()` 默认 `ensure_ascii=True`,将所有非 ASCII 字符转义为 `\uXXXX` 格式。 **解决**:显式设置 `ensure_ascii=False`。 ```python json.dumps(data, ensure_ascii=False) # {"city": "大连"}
[!WARNING] 错误 2:尝试序列化不支持的类型(如 datetime)
import json from datetime import datetime data = {"time": datetime.now()} json.dumps(data) # 💥 TypeError: Object of type datetime is not JSON serializable原因:
json模块默认不支持datetime、自定义类等复杂类型的序列化。解决:自定义编码器或使用
default参数:json.dumps(data, default=str) # ✅ 将 datetime 转为字符串 # 或定义完整的 CustomEncoder 类(见上方示例 2)
最佳实践
- 写文件用
encoding="utf-8"— JSON 文件读写时必须显式指定 UTF-8 编码,避免不同操作系统上的默认编码差异导致乱码 - 对不可信输入使用
try-except包裹json.loads()— 解析外部来源的 JSON 字符串时,格式可能不符合规范,捕获json.JSONDecodeError避免程序崩溃
练习
- 编写一个函数
save_config(config_dict, filepath),将配置字典保存为格式化的 JSON 文件,包含中文支持。
查看答案
import json
def save_config(config_dict, filepath):
with open(filepath, "w", encoding="utf-8") as f:
json.dump(config_dict, f, ensure_ascii=False, indent=2)
- 编写一个自定义编码器,能将 Python
set类型序列化为 JSON 数组。
查看答案
import json
class SetEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj) # 将 set 转为 list
return super().default(obj)
data = {"tags": {"Python", "数据分析", "机器学习"}, "count": 3}
print(json.dumps(data, cls=SetEncoder, ensure_ascii=False))
知识检查
-
json.dumps(data, ensure_ascii=False)中ensure_ascii=False的作用是?- A. 加快序列化速度
- B. 允许非 ASCII 字符(如中文)直接输出
- C. 跳过字典中的 Unicode 键
- D. 启用 UTF-16 编码
-
以下哪个是
json模块将文件读取的正确方式?- A.
json.loads() - B.
json.load() - C.
json.read() - D.
json.parse()
- A.
-
Python 的
None在 JSON 中等价于什么?- A.
"null" - B.
null - C.
undefined - D.
empty
- A.
查看答案
- B — 默认
ensure_ascii=True会将中文等转为\uXXXX转义序列 - B —
load()从文件对象读取,loads()从字符串解析 - B — JSON 中的
null(小写,无引号)对应 Python 的None
本章小结
json.dumps()/json.loads()处理字符串,json.dump()/json.load()处理文件ensure_ascii=False使中文正常输出,indent=N美化排版- 自定义
JSONEncoder的default()方法可以处理 datetime、自定义类等复杂对象 - JSON 数据类型与 Python 类型一一映射:object→dict、array→list、null→None
- 解析外部 JSON 数据时务必处理
JSONDecodeError异常
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| JSON | JavaScript 对象表示法 | 一种轻量级数据交换格式,易于人阅读和编写 |
| serialization | 序列化 | 将 Python 对象转换为 JSON 字符串(或字节)的过程 |
| deserialization | 反序列化 | 将 JSON 字符串解析为 Python 对象的过程 |
| JSONEncoder | JSON 编码器 | Python 中负责将对象序列化为 JSON 格式的基类 |
| custom encoder | 自定义编码器 | 继承 JSONEncoder 重写 default() 方法处理特殊类型的编码器 |
下一步
- NumPy 数值计算 → 学习使用 NumPy 进行高效的数值计算和数组操作
源码链接
NumPy 数值计算 (NumPy Numerical Computing)
导语
假设你需要处理一百万个温度传感器的读数,计算每个读数的偏移量、找最大值、做统计分析。如果用纯 Python 循环,程序可能需要运行数秒甚至数十秒。但若使用 NumPy,同样的操作只需几毫秒——速度差距可达 100 倍以上。这就是 NumPy(Numerical Python)的威力:它通过 C 语言实现的底层数组和向量化操作,让 Python 拥有了接近 C/Fortran 的数值计算性能。无论是机器学习、科学计算、金融分析还是图像处理器,NumPy 都是 Python 数据科学生态系统中不可或缺的基石。
学习目标
- 理解 NumPy 数组(ndarray)与 Python 列表的本质区别及性能优势
- 掌握数组的广播(broadcasting)、逐元素运算和索引切片
- 学会使用 NumPy 实现梯度下降等经典数值算法
概念介绍
NumPy 的核心数据结构是 n 维数组(ndarray)。与 Python 原生列表相比,ndarray 有几个关键差异:
- 同质性 — ndarray 中所有元素必须是相同类型(如全部 float64),这避免了 Python 列表中每个元素都要存储类型信息的开销。
- 连续性内存 — 数组数据在内存中是连续存储的,CPU 缓存命中率更高,批量操作速度更快。
- 向量化 — 对数组的算术运算会自动应用到每个元素,无需写
for循环。这种"批量处理"思维是科学计算的核心范式。 - 广播(Broadcasting) — NumPy 允许不同形状的数组进行运算。例如一个二维矩阵可以和一个一维向量直接相加——NumPy 会自动将向量"扩展"以匹配矩阵的形状,无需手动复制数据。
[!NOTE] NumPy 是几乎整个 Python 数据科学生态的"地基":Pandas 内部使用 ndarray 存储数据,scikit-learn 的机器学习算法依赖 NumPy 数组,Matplotlib 的绘图数据也来自 ndarray。学会 NumPy,就等于打开了 Python 数据科学的大门。
代码示例
示例 1:NumPy 数组与 Python 列表的对比
import numpy as np
# 从 Python 列表创建 NumPy 数组
py_list = [1, 2, 3, 4, 5]
np_array = np.array(py_list)
print(f"Python 列表: {py_list}") # [1, 2, 3, 4, 5]
print(f"NumPy 数组: {np_array}") # [1 2 3 4 5]
print(f"数组类型: {np_array.dtype}") # int64
# 创建特殊数组
zeros = np.zeros((3, 3)) # 3x3 全零矩阵
ones = np.ones((2, 4)) # 2x4 全一矩阵
random_arr = np.random.randn(5) # 5 个标准正态分布随机数
linspace = np.linspace(0, 1, 5) # 0 到 1 均匀取 5 个点
# 数组属性
matrix = np.array([[1, 2], [3, 4], [5, 6]])
print(f"形状(shape): {matrix.shape}") # (3, 2)
print(f"维度(ndim): {matrix.ndim}") # 2
print(f"元素数(size): {matrix.size}") # 6
np.array() 从列表创建数组,dtype 属性自动推断元素类型。创建数组时,np.zeros((rows, cols)) 和 np.ones((rows, cols)) 比用列表推导式更高效。shape 返回元组表示各维度大小,ndim 是维度数,size 是总元素数。
示例 2:向量化运算与广播
import numpy as np
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
# 逐元素运算(向量化,无需 for 循环)
print(a + b) # [11 22 33 44]
print(a * b) # [10 40 90 160]
print(a ** 2) # [1 4 9 16]
print(np.sqrt(a)) # [1. 1.414 1.732 2. ]
# 广播:不同形状的数组自动对齐
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
vector = np.array([10, 20, 30])
result = matrix + vector # vector 沿行方向广播到每一行
print(result)
# [[11 22 33]
# [14 25 36]
# [17 38 39]]
# 矩阵乘法和统计
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(a @ b) # 矩阵乘法 [[19 22] [43 50]]
print(a.mean(), a.std()) # 均值 2.5, 标准差 1.118...
print(a.sum(axis=0)) # 按列求和 [4 6]
向量化运算的精髓在于:表达式直接对应数学公式,无需嵌套循环。a + b 等价于 [x + y for x, y in zip(a, b)],但 NumPy 在 C 层面批量处理,速度快两个数量级。广播规则看似"魔法",实则是 NumPy 自动在较小数组的前面补 1 维,然后沿该维重复数据以匹配较大数组的形状。
[!TIP] 当你的代码中出现
for循环逐个处理数组元素时,考虑是否能用向量化操作替代。向量化代码不仅更快,而且更简洁易读。
示例 3:梯度下降算法实现
import numpy as np
def objective_function(x):
"""目标函数:f(x) = (x - 3)^2"""
return (x - 3) ** 2
def gradient(x):
"""目标函数的梯度:f'(x) = 2 * (x - 3)"""
return 2 * (x - 3)
def gradient_descent(learning_rate=0.1, tolerance=1e-6, max_iter=1000):
x = np.random.randn() # 随机起点
prev_value = objective_function(x)
for i in range(max_iter):
grad = gradient(x)
x -= learning_rate * grad # 沿负梯度方向更新
curr_value = objective_function(x)
print(f"Iteration {i+1}: x = {x:.6f}, f(x) = {curr_value:.6f}")
if abs(curr_value - prev_value) < tolerance:
print(f"收敛!经过 {i+1} 次迭代。")
break
prev_value = curr_value
return x
# 运行梯度下降
optimal_x = gradient_descent()
print(f"最优解 x = {optimal_x:.6f}")
# 期望输出: x ≈ 3.0, f(x) ≈ 0(因为 f(3) = 0 是最小值点)
梯度下降是机器学习中最基础的优化算法。核心思想:从随机起点开始,每步计算当前点的梯度(函数的"坡度"),沿负梯度方向(下坡方向)走一小步(步长 = 学习率),迭代直到值的变化小于阈值。learning_rate 控制步长——太大可能震荡不收敛,太小收敛太慢。NumPy 在这里虽未显式展示数组运算,但其 np.random.randn() 提供了随机数生成能力。
常见错误与解决
[!WARNING] 错误 1:数组形状不匹配导致广播失败
import numpy as np a = np.array([[1, 2, 3]]) # 形状 (1, 3) b = np.array([[1], [2]]) # 形状 (2, 1) c = a + b # ✅ 广播成功,结果形状 (2, 3) d = np.array([1, 2, 3]) # 形状 (3,) e = np.array([1, 2]) # 形状 (2,) # f = d + e # 💥 ValueError: operands could not be broadcast together原因:广播要求两个数组从后往前逐维比较,每维必须相等或其中一个为 1。长度 3 和 2 的数组无法匹配。
解决:确保数组形状兼容,或使用
reshape()调整维度。e_reshaped = e.reshape(2, 1) # 转为 (2, 1),可与 (1, 3) 广播
[!WARNING] 错误 2:误用
*做矩阵乘法import numpy as np a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) print(a * b) # 💥 逐元素相乘: [[5 12] [21 32]] print(a @ b) # ✅ 矩阵乘法: [[19 22] [43 50]]原因:NumPy 中
*是逐元素乘(Hadamard 积),不是线性代数中的矩阵乘法。解决:矩阵乘法用
@运算符或np.dot(a, b)。@在 Python 3.5+ 中可用,语法更直观。
最佳实践
- 优先使用向量化操作替代循环 — NumPy 的内置函数(
+,*,np.sum(),np.mean()等)在 C 层实现,速度远超 Pythonfor循环 - 理解广播规则,善用隐式扩展 — 不要用
np.tile()或np.repeat()手动复制数组来实现广播,让 NumPy 自动处理,节省内存和计算
练习
- 使用 NumPy 计算一组学生成绩
scores = [78, 92, 65, 88, 71, 95]的平均分、标准差、最高分和最低分。
查看答案
import numpy as np
scores = np.array([78, 92, 65, 88, 71, 95])
print(f"平均分: {scores.mean():.1f}")
print(f"标准差: {scores.std():.1f}")
print(f"最高分: {scores.max()}")
print(f"最低分: {scores.min()}")
- 使用梯度下降编写一个线性回归拟合器:给定数据点
x = [1, 2, 3, 4, 5],y = [2, 4, 5, 4, 5],用y = w*x + b拟合,迭代更新w和b。
查看答案
import numpy as np
x = np.array([1, 2, 3, 4, 5], dtype=float)
y = np.array([2, 4, 5, 4, 5], dtype=float)
w, b = 0.0, 0.0
lr = 0.01
n = len(x)
for i in range(1000):
pred = w * x + b
error = pred - y
dw = (2 / n) * np.sum(error * x)
db = (2 / n) * np.sum(error)
w -= lr * dw
b -= lr * db
print(f"拟合结果: y = {w:.3f}*x + {b:.3f}")
知识检查
-
NumPy 数组中所有元素必须是?
- A. 相同的值
- B. 相同的类型
- C. 相同的维度
- D. 相同的形状
-
以下哪个操作用于 NumPy 矩阵乘法?
- A.
a * b - B.
a @ b - C.
a × b - D.
a.mul(b)
- A.
-
学习率(learning rate)在梯度下降中的作用是?
- A. 决定迭代次数
- B. 控制每次更新的步长
- C. 决定初始参数的随机种子
- D. 检测收敛的阈值
查看答案
- B — NumPy 数组是同质类型的,这与 Python 列表不同
- B —
@是矩阵乘法运算符,*是逐元素乘 - B — 学习率控制沿负梯度方向每次移动的距离,过大则震荡,过小则收敛慢
本章小结
- NumPy 的 ndarray 是同类型、连续内存的高效数组结构,性能远超 Python 列表
- 向量化运算无需
for循环,a + b直接对每个元素操作,代码简洁且极速 - 广播机制允许不同形状的数组进行运算,避免手动重复数据
- 矩阵乘法用
@而非*(*是逐元素乘) - 梯度下降是优化算法的基础,通过迭代更新参数逼近最优解
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| NumPy | 数值 Python | Python 科学计算的基础库,提供高效的多维数组和数学函数 |
| ndarray | n 维数组 | NumPy 的核心数据结构,同质类型的连续内存数组 |
| gradient descent | 梯度下降 | 通过沿负梯度方向迭代更新参数以最小化目标函数的优化算法 |
| learning rate | 学习率 | 梯度下降中控制每步更新幅度的超参数 |
| convergence | 收敛 | 迭代过程中目标函数值变化小于阈值,算法停止 |
下一步
- 阶段复习:进阶部分 → 回顾整个进阶部分的知识,检验学习成果
源码链接
阶段复习:进阶部分 (Review: Advanced Python)
恭喜到达进阶部分的最后一站!在本章中,我们将回顾进阶阶段涉及的全部知识领域,通过综合练习和自测题检验你的学习成果。
导语
进阶部分涵盖了 Python 中最实用的几个高级主题:异步编程让你能同时处理大量并发任务,FastAPI 帮助你快速构建现代 Web API,依赖注入让你的代码更可测试和可维护,数据库操作让你的程序具备数据持久化能力,JSON 是网络数据交换的通用语言,NumPy 则是科学计算和数据分析的基石。这七个主题看似独立,但在实际项目中常常组合使用——例如一个 FastAPI API 可能需要异步连接数据库、序列化 JSON 响应、甚至调用 NumPy 做数据处理。复习的目的不是"再看一遍",而是帮你建立知识之间的连接,形成完整的认知地图。
学习目标
- 系统回顾进阶部分的 7 个知识领域,查漏补缺
- 通过综合练习将多个知识点融会贯通
- 通过自测题检验理解深度,为后续学习奠定基础
知识回顾清单
对照以下清单,确认你对每个知识点有清晰的理解。如果某个条目让你犹豫,请回到对应章节重新学习。
1. 异步编程 (asyncio)
-
async/await语法的工作机制 -
asyncio.gather()并发运行多个协程 -
asyncio.create_task()后台调度任务 -
asyncio.wait_for()超时控制 -
ThreadPoolExecutor处理 CPU 密集型任务 - 避免在异步代码中使用阻塞调用
2. FastAPI 路由与请求处理 (FastAPI Routes)
- 定义 GET/POST/PUT/DELETE 路由
- 路径参数(Path Parameters)和查询参数(Query Parameters)
- 请求体(Request Body)与 Pydantic 模型验证
- 响应模型(Response Model)和数据过滤
3. FastAPI 服务器 (FastAPI Server)
- 完整的 FastAPI 应用生命周期
- 中间件(Middleware)和异常处理(Exception Handlers)
- APIRouter 模块化路由组织
- 异步 API 端点的实现
4. 依赖注入 (Dependency Injection)
- DI(依赖注入)的核心思想:控制反转
-
FastAPI 的
Depends()机制 - injector 库的基本使用
- 依赖注入如何提高代码的可测试性
5. 数据库操作 (Database)
- SQLite 内存数据库的 CRUD 操作
- PyMySQL 连接 MySQL 数据库的流程
- 游标(cursor)的作用和使用方式
- 参数化查询防止 SQL 注入
-
commit()和事务管理
6. JSON 数据处理 (JSON)
-
json.dumps()/json.loads()序列化与反序列化 -
ensure_ascii=False的中文字符处理 -
自定义
JSONEncoder处理特殊类型(datetime、自定义类) -
JSON 文件的读写(
json.dump()/json.load())
7. NumPy 数值计算 (NumPy)
- ndarray 与 Python 列表的区别
- 向量化运算和广播(broadcasting)
-
矩阵乘法
@vs 逐元素乘* - 梯度下降算法的基本原理
- 学习率对收敛的影响
综合练习
练习 1:异步 FastAPI API + SQLite 数据库
编写一个 FastAPI 应用,实现以下功能:
- 连接 SQLite 内存数据库,创建一个
todos表(id, title, completed) GET /todos— 获取所有待办事项,以 JSON 格式返回POST /todos— 创建新的待办事项,接收 JSON 请求体{title: "..."}- 所有数据库操作使用参数化查询
查看答案
import sqlite3
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
def get_connection():
"""获取数据库连接(内存)。"""
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0
)
""")
conn.commit()
return conn
class TodoCreate(BaseModel):
title: str
class Todo(BaseModel):
id: int
title: str
completed: bool
@app.get("/todos", response_model=List[Todo])
async def get_todos():
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT id, title, completed FROM todos")
rows = cursor.fetchall()
conn.close()
return [{"id": r[0], "title": r[1], "completed": bool(r[2])} for r in rows]
@app.post("/todos", response_model=Todo)
async def create_todo(todo: TodoCreate):
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO todos (title) VALUES (?)", (todo.title,)
)
conn.commit()
todo_id = cursor.lastrowid
conn.close()
return {"id": todo_id, "title": todo.title, "completed": False}
练习 2:NumPy 数据处理 + JSON 序列化
给定一个学生成绩数组,使用 NumPy 计算:每个学生各科平均分、各科最高分、成绩标准差。将结果序列化为 JSON 格式(包含中文),保存到文件。
查看答案
import numpy as np
import json
# 5个学生3门课程的成绩矩阵
scores = np.array([
[85, 90, 78],
[92, 88, 95],
[76, 82, 80],
[95, 91, 88],
[88, 75, 82],
])
# NumPy 计算
student_avg = scores.mean(axis=1) # 每个学生平均分
subject_max = scores.max(axis=0) # 每科最高分
std = scores.std() # 全部成绩标准差
# 汇总结果
results = {
"学生平均分": [float(x) for x in student_avg],
"各科最高分": [int(x) for x in subject_max],
"成绩标准差": float(std),
}
# JSON 序列化
json_str = json.dumps(results, ensure_ascii=False, indent=2)
print(json_str)
# 保存文件
with open("data/scores_report.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
自测题
1. 以下哪个场景最适合使用 asyncio?
A. 需要大量 CPU 计算的科学模拟 B. 同时发起 1000 个 HTTP 请求获取数据 C. 排序一个包含 100 万个元素的列表 D. 在本地计算文件哈希值
2. 在 FastAPI 中,路径参数和查询参数的区别是什么?
A. 路径参数在 URL 路径中(如 /users/{id}),查询参数在 URL 末尾(如 ?skip=0&limit=10)
B. 路径参数更安全,查询参数更灵活
C. 两者完全等价,只是写法不同
D. 路径参数只能是字符串,查询参数可以是任意类型
3. 以下哪个选项不是数据库参数化查询的好处?
A. 防止 SQL 注入攻击 B. 自动优化查询执行计划 C. 数据库驱动正确处理数据类型转义 D. 代码更易读和维护
4. NumPy 数组 a = np.array([[1, 2], [3, 4]]) 与 b = np.array([10, 20]) 执行 a + b 后,结果是?
A. 广播失败,抛出 ValueError
B. [[11, 22], [23, 24]]
C. [[11, 12], [23, 24]]
D. [[11, 22], [13, 24]]
5. json.dumps({"data": datetime.now()}, default=str) 的作用是什么?
A. 将 datetime 转为特定格式
B. 用 str() 函数将 datetime 序列化为字符串
C. 跳过 datetime 字段不序列化
D. 使用默认的 JSON 编码器
查看答案
- B — asyncio 适合 I/O 密集型并发(大量网络请求),不适合 CPU 密集型任务
- A — 路径参数是 URL 路径的一部分(
/users/{user_id}),查询参数以?开头附加在 URL 末尾 - B — 参数化查询主要好处是安全性和类型安全,执行计划优化由数据库自行决定,不是参数化的直接好处
- D —
b = [10, 20]广播到a的每一行:[[1,2]+[10,20], [3,4]+[10,20]] = [[11,22],[13,24]] - B —
default=str是json.dumps()的参数,当遇到无法直接序列化的对象时调用str()转换
下一步
你已经完成了进阶部分的全部学习!以下是继续深入的建议路径:
回顾与巩固:
- 异步编程 — 重温协程与事件循环
- FastAPI 路由与请求处理 — 回顾 Web API 路由设计
- FastAPI 服务器 — 回顾完整 API 服务器构建
- 依赖注入 — 回顾 DI 模式
- 数据库操作 — 重温 SQLite 和 PyMySQL
- JSON 数据处理 — 回顾序列化与自定义编码器
- NumPy 数值计算 — 重温向量化与数值算法
进阶方向:
- 探索 FastAPI 的认证与授权(JWT、OAuth2)
- 学习 SQLAlchemy ORM 进行更高级的数据库操作
- 尝试异步数据库驱动(如
asyncpg、aiomysql) - 深入 NumPy 和 Pandas 进行数据分析实战