About Hello Python
一个编程高手是怎样练成的?惟手熟尔,重在刻意练习。 这意味着不断学习、练习、实践、再实践,直到熟练掌握所有技能。
Hello,Python 是如何产生的?这是我在学习 Python 过程中,不断编写样例代码、积累点滴经验,最终形成的一套系统教程。
Python 是一门非常优秀的编程语言,它语法简洁、可读性强、功能强大,广泛应用于 Web 开发、数据分析、机器学习、自动化脚本、人工智能等领域。对于初学者来说,Python 是入门首选语言——它的语法接近自然语言,学习曲线平缓。
Hello,Python 是一个绝佳的学习起点。通过这个项目,你不仅能快速入门 Python 编程,还能通过编写、调试、运行示例代码,迅速掌握 Python 的核心知识点。它涵盖了从基础语法到高级进阶知识的完整学习路径,包括异步编程、Web 框架(FastAPI)、数据库操作、NumPy 数值计算等内容。
本书使用 Python 3.10+ 编写(测试环境为 Python 3.13.3),包管理器使用 uv,文档使用 mdBook 构建。请查看 Getting Started 了解如何安装和配置开发环境。
下一步
- 简介 → 了解 Python 语言的历史、设计哲学和应用领域
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 上均可运行
- 开源:完全免费,社区活跃
- 生产力高:几行代码即可完成其他语言数十行代码的功能
下一步
- 快速开始 → 配置开发环境,安装 Python 和 uv 包管理器
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)。
示例 4:身份比较 is vs 值比较 ==
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — 值相等
print(a is b) # False — 不是同一个对象
print(a is c) # True — c 和 a 指向同一对象
# 小整数缓存(-5 到 256)
x = 256
y = 256
print(x is y) # True — Python 缓存了小整数
x = 257
y = 257
print(x is y) # False(交互式环境可能为 True,取决于实现)
warning
常见误区:用 is 比较值
is 检查的是身份(是否是同一个对象),== 检查的是值(内容是否相等)。
除非你在检查 None(推荐 x is None),否则应该用 == 比较值。
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:字符串索引与切片
text = "Python"
# 索引 — 从 0 开始计数
print(text[0]) # P — 第一个字符
print(text[-1]) # n — 最后一个字符
# 切片 — [start:stop:step],stop 不包含
print(text[0:2]) # Py — 从索引 0 到 1
print(text[2:]) # thon — 从索引 2 到最后
print(text[:4]) # Pyth — 从开头到索引 3
print(text[::-1]) # nohtyP — 反转字符串
print(text[::2]) # Pto — 每隔一个取一个
tip
切片不会越界报错——text[0:100] 会安全地返回整个字符串,这与列表切片行为一致。
示例 4:字符串方法
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}
示例 5:列表常用方法
fruits = ["apple", "banana"]
# 添加元素
fruits.append("cherry") # 末尾添加 → ['apple', 'banana', 'cherry']
fruits.insert(1, "apricot") # 指定位置插入 → ['apple', 'apricot', 'banana', 'cherry']
fruits.extend(["date", "fig"]) # 批量追加
# 删除元素
removed = fruits.pop() # 弹出最后一个 → 'fig'
fruits.remove("banana") # 按值删除
del fruits[0] # 按索引删除
# 查找与统计
idx = fruits.index("apricot") # 返回索引 0
count = fruits.count("apple") # 返回出现次数
# 排序与反转
nums = [3, 1, 4, 1, 5]
nums.sort() # 原地排序 → [1, 1, 3, 4, 5]
nums.reverse() # 原地反转 → [5, 4, 3, 1, 1]
tip
.sort() 是原地排序(修改原列表),sorted() 返回新列表。需要保留原数据时用 sorted()。
示例 6:字典常用方法
user = {"name": "Alice", "age": 30, "city": "Shanghai"}
# 安全访问
name = user.get("name") # 'Alice'
missing = user.get("phone", "N/A") # 'N/A' — 键不存在时返回默认值
# 遍历
for key in user.keys(): # 遍历键
pass
for value in user.values(): # 遍历值
pass
for k, v in user.items(): # 同时遍历键值
print(f"{k}: {v}")
# 修改与删除
user.update({"age": 31, "role": "admin"}) # 批量更新
user.pop("city") # 删除并返回值
user.setdefault("role", "user") # 键不存在时设默认值
# Python 3.9+ 合并运算符
defaults = {"theme": "light", "lang": "zh"}
config = defaults | {"theme": "dark"} # 合并,右侧覆盖左侧
warning
错误:直接遍历字典时修改大小
for k in d:
d[k + "_new"] = d[k] # 💥 RuntimeError: dictionary changed size
解决:先收集要添加的键值,遍历结束后再更新;或遍历 list(d.keys()) 的副本。
知识检查
-
[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 框架、数据库等更强大的内容。
下一步
- 进阶入门 → 开始进阶部分的学习,探索异步编程、FastAPI、数据库等内容
进阶入门 (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 | 装饰器 | ⭐⭐⭐ | 20 分钟 | @ 语法、functools.wraps、装饰器带参数 |
| 9 | 生成器与迭代 | ⭐⭐⭐ | 20 分钟 | yield、生成器表达式、惰性计算 |
| 10 | 上下文管理器 | ⭐⭐ | 15 分钟 | with 语句、enter/exit、contextlib |
| 11 | 类型提示 | ⭐⭐ | 15 分钟 | 类型注解、typing 模块、TypedDict、Protocol |
| 12 | 数据类 | ⭐⭐ | 15 分钟 | @dataclass、field()、frozen、post_init |
| 13 | 阶段复习:进阶部分 | — | 30 分钟 | 综合复习与自测 |
tip
全部 12 个进阶章节预计学习时长约 4.5 小时。每章都配有练习题和自测题。
为什么学这些?
- 异步编程 → 让你的程序在等待 I/O 时不阻塞,大幅提升性能
- FastAPI → Python 中最受欢迎的现代 Web 框架,几行代码就能构建 REST API
- 依赖注入 → 解耦代码、提升可测试性、适用于大型项目
- 数据库 → 与 MySQL 和 SQLite 交互,构建有状态的应用
- JSON → 数据交换的事实标准,前后端通信的桥梁
- NumPy → 科学计算和机器学习的基石
- 装饰器 → 用
@语法优雅地增强函数功能,日志、缓存、权限控制一行搞定 - 生成器 → 惰性计算,处理无限序列或大数据流时内存占用趋近于零
- 上下文管理器 → 确保资源(文件、连接、锁)始终正确释放,杜绝泄漏
- 类型提示 → 让 IDE 自动补全更精准,mypy 静态检查在运行前捕获 bug
- 数据类 → 告别冗长的
__init__/__repr__/__eq__,一行定义数据模型
下一步
从 异步编程 开始你的进阶之旅!
异步编程 (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))
</div>
# 输出: {"city": "\u5927\u8fde"} — 中文变成了 Unicode 转义序列
> ```
>
> **原因**:`json.dumps()` 默认 `ensure_ascii=True`,将所有非 ASCII 字符转义为 `\uXXXX` 格式。
>
> **解决**:显式设置 `ensure_ascii=False`。
>
> ```python
> json.dumps(data, ensure_ascii=False) # {"city": "大连"}
> ```
<div class="mdbook-alerts mdbook-alerts-warning">
<p class="mdbook-alerts-title">
<span class="mdbook-alerts-icon"></span>
warning
</p>
**错误 2:尝试序列化不支持的类型(如 datetime)**
```python
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 | 收敛 | 迭代过程中目标函数值变化小于阈值,算法停止 |
下一步
- 阶段复习:进阶部分 → 回顾整个进阶部分的知识,检验学习成果
源码链接
装饰器 (Decorators)
导语
装饰器是 Python 中一种极具表现力的设计模式,它允许你在不修改原始函数代码的前提下,为其增加额外的功能。想象你编写了一系列核心业务函数,现在需要为每个函数添加性能监控、日志记录、权限校验或缓存功能——如果不使用装饰器,你需要逐一修改每个函数的内部逻辑,这不仅繁琐而且容易出错。装饰器通过 @ 语法糖,将这些"横切关注点"(cross-cutting concerns)以声明式的方式"包裹"在函数外部,让你的代码既保持纯净又功能丰富。在 FastAPI 路由定义、Flask 视图保护、Django 中间件、Celery 任务调度等主流框架中,装饰器都是不可或缺的核心机制。
学习目标
- 理解函数作为一等对象(first-class object)的含义及其在装饰器中的应用
- 掌握装饰器的语法糖
@及其等价的手动包裹写法 - 学会使用
functools.wraps保留被装饰函数的元数据,以及编写带参数的装饰器
概念介绍
理解装饰器需要先掌握一个核心前提:Python 中函数是一等对象。这意味着函数可以像普通变量一样被赋值、传递、作为参数传入、作为返回值返回,甚至存储在数据结构中。
-
函数作为一等对象 — 函数没有"特权",它和整数、字符串一样是对象。你可以把函数赋值给另一个变量(
f = my_func),可以把函数作为参数传给另一个函数(call_it(my_func)),也可以从函数中返回另一个函数。这种灵活性是装饰器能工作的基础。 -
@语法糖 —@decorator本质上是一种"语法糖":@decorator def func(): pass # 等价于: def func(): pass func = decorator(func)@语法在函数定义后立即用装饰器对象重新绑定函数名,使得增强逻辑的意图一目了然。 -
包装函数(Wrapper) — 装饰器本质上是一个接收函数作为参数并返回函数的"高阶函数"。内部通常定义一个
wrapper函数,在其中执行额外逻辑(如计时、日志),然后通过*args, **kwargs将参数透传给原始函数。 -
functools.wraps— 如果装饰器直接返回一个新的wrapper函数,原始函数的__name__、__doc__、__signature__等元数据将丢失。@functools.wraps(original_func)装饰器会将原始函数的元数据复制到wrapper上,确保调试、文档生成、内省等操作正常工作。 -
带参数的装饰器 — 当需要向装饰器传递配置参数(如
@repeat(3)),实际需要一个三层嵌套:最外层接收装饰器参数,中间层接收被装饰函数,最内层是执行逻辑的wrapper。
tip
判断装饰器层数的方法:如果 @ 后面直接是函数名(如 @timer),是两层结构(装饰器函数 + wrapper);如果 @ 后面是函数调用(如 @repeat(3)),则需要三层结构。
代码示例
示例 1:基础装饰器 — 计时与日志
import time
def timer(func):
"""测量函数执行时间的装饰器。"""
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[TIMER] {func.__name__} took {elapsed:.4f} seconds.")
return result
return wrapper
def log_call(func):
"""记录函数调用参数的装饰器。"""
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__}(args={args}, kwargs={kwargs})")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@timer
def compute_sum(n):
total = sum(range(n))
return total
@log_call
def greet(name):
return f"Hello, {name}!"
@timer
@log_call
def process_data(items):
return [item * 2 for item in items]
if __name__ == "__main__":
print(compute_sum(1_000_000))
# [LOG] Calling compute_sum(args=(1000000,), kwargs={})
# [TIMER] compute_sum took 0.0012 seconds.
# 499999500000
print(greet("Python"))
# [LOG] Calling greet(args=('Python',), kwargs={})
# [LOG] greet returned Hello, Python!
# Hello, Python!
print(process_data([1, 2, 3]))
# [LOG] Calling process_data(args=([1, 2, 3],), kwargs={})
# [LOG] process_data returned [2, 4, 6]
# [TIMER] process_data took 0.0001 seconds.
# [2, 4, 6]
*args 和 **kwargs 使得 wrapper 可以接收任意数量和类型的参数,并原封不动地转发给原始函数。多个装饰器可以叠加使用(stack),叠加顺序是从下往上:@timer 包裹 @log_call 包裹 process_data,执行时 timer 最先启动计时,log_call 随后记录日志,最后执行原始函数。
tip
装饰器的叠加顺序会影响执行行为。@A @B def f() 等价于 f = A(B(f)),即 B 先包裹 f,然后 A 再包裹结果。调用时 A 的逻辑最先执行,B 的逻辑次之。
示例 2:使用 functools.wraps 保留函数元数据
import functools
def sloppy_decorator(func):
"""未使用 wraps 的装饰器 — 元数据丢失。"""
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def proper_decorator(func):
"""使用了 wraps 的装饰器 — 元数据保留。"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@sloppy_decorator
def bad_function():
"""This docstring will be lost."""
pass
@proper_decorator
def good_function():
"""This docstring is preserved."""
pass
if __name__ == "__main__":
print(f"sloppy: name={bad_function.__name__}, doc={bad_function.__doc__}")
# sloppy: name=wrapper, doc=None
print(f"proper: name={good_function.__name__}, doc={good_function.__doc__}")
# proper: name=good_function, doc=This docstring is preserved.
不使用 @functools.wraps 时,被装饰函数的 __name__ 变成了 "wrapper",__doc__ 变成了 None。这会破坏依赖元数据的工具(如 Flask 路由系统用函数名生成端点名,或 Sphinx 自动生成文档)。
warning
永远不要在装饰器中遗漏 @functools.wraps(func)。缺少 wraps 会导致调试困难(堆栈跟踪显示错误的函数名)、文档工具失效,以及某些框架(如 Flask、FastAPI)的路由注册异常。
示例 3:带参数的装饰器 — 重复执行与超时配置
import functools
import time
def repeat(times):
"""重复执行被装饰函数指定次数的装饰器工厂。"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for i in range(times):
print(f"[REPEAT] Execution {i + 1}/{times}: calling {func.__name__}()")
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
def timeout(max_seconds):
"""为函数设置超时限制的装饰器工厂。
注意:此实现使用信号(signal)仅支持 Unix 平台。"""
import signal
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
def raise_timeout(signum, frame):
raise TimeoutError(f"Function {func.__name__} timed out ({max_seconds}s).")
old_handler = signal.signal(signal.SIGALRM, raise_timeout)
signal.alarm(max_seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old_handler)
return result
return wrapper
return decorator
@repeat(times=3)
def roll_dice():
import random
return random.randint(1, 6)
@timeout(max_seconds=2)
def slow_task():
time.sleep(5)
return "completed"
if __name__ == "__main__":
print(f"Dice results: {roll_dice()}")
# [REPEAT] Execution 1/3: calling roll_dice()
# [REPEAT] Execution 2/3: calling roll_dice()
# [REPEAT] Execution 3/3: calling roll_dice()
# Dice results: [4, 2, 6]
# slow_task() # Uncomment on Unix to see: TimeoutError
# TimeoutError: Function slow_task timed out (2s).
带参数的装饰器需要三层嵌套:最外层 repeat(times) 接收参数并返回装饰器函数,中间层 decorator(func) 接收被装饰函数,最内层 wrapper 是实际执行的代码。@repeat(times=3) 先执行 repeat(3) 得到一个装饰器,再将 roll_dice 传入。
note
signal.alarm 仅在 Unix/Linux/macOS 上可用(Windows 不支持 SIGALRM)。生产中更推荐使用 concurrent.futures.TimeoutError 或第三方库 func-timeout 实现跨平台超时控制。
常见错误与解决
warning
错误 1:遗漏 @functools.wraps 导致元数据丢失
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""Important documentation."""
pass
print(my_function.__name__) # wrapper — 不是 my_function!
print(my_function.__doc__) # None — 文档字符串丢失!
原因:装饰器返回的 wrapper 是一个全新的函数对象,没有继承原始函数的元数据。
解决:为 wrapper 添加 @functools.wraps(func) 装饰器,将原始函数的 __name__、__doc__ 等属性复制到 wrapper 上。
warning
错误 2:忽略装饰器顺序导致行为不一致
def uppercase(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
def exclamation(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!"
return wrapper
@uppercase
@exclamation
def message():
return "hello"
print(message()) # HELLO! — exclamation 先给 hello 加 !,uppercase 再把整个字符串转大写
# 交换顺序后:
@exclamation
@uppercase
def message():
return "hello"
print(message()) # HELLO! — uppercase 先转大写,exclamation 再加 !
原因:装饰器从下往上执行包裹过程,调用时从上往下执行 wrapper 逻辑。顺序不同,组合效果也不同。
解决:理解装饰器执行流程——@A @B def f() 等价于 A(B(f))。B 先包裹 f(进入时 B 的逻辑先执行),A 再包裹结果(A 在 B 的外层)。对于幂等性不强的装饰器组合,务必仔细验证执行顺序。
最佳实践
- 始终使用
@functools.wraps— 在每个装饰器的wrapper函数上应用@functools.wraps(func),保留原始函数的元数据,确保调试和文档工具正常工作 - 保持装饰器职责单一 — 每个装饰器只负责一个增强功能(计时、缓存、日志、鉴权等),通过组合多个装饰器实现复杂逻辑,而非在单个装饰器中堆砌功能
- 注意闭包变量捕获 — 当在循环中创建装饰器或闭包时,使用默认参数(如
def wrapper(_i=i))避免"延迟绑定"(late binding)问题,否则所有 wrapper 可能捕获到循环变量的最终值
练习
- 编写一个
@cache_result装饰器,使用字典缓存函数在特定参数下的返回值。如果相同参数再次调用,直接返回缓存结果而非重新执行函数。
查看答案
import functools
def cache_result(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
if key not in wrapper._cache:
wrapper._cache[key] = func(*args, **kwargs)
return wrapper._cache[key]
wrapper._cache = {}
return wrapper
@cache_result
def expensive_computation(n):
"""模拟耗时计算。"""
print(f"Computing for {n}...")
result = 0
for i in range(n):
result += i * i
return result
print(expensive_computation(100))
# Computing for 100...
# 328350
print(expensive_computation(100))
# (no output — cached result returned)
# 328350
print(expensive_computation(200))
# Computing for 200...
# 2653300
- 编写一个
@validate_types装饰器,利用函数的__annotations__检查传入参数类型是否匹配,不匹配时抛出TypeError。
查看答案
import functools
def validate_types(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
hints = func.__annotations__
params = func.__code__.co_varnames[: func.__code__.co_argcount]
for param_name, value in zip(params, args):
if param_name in hints and not isinstance(value, hints[param_name]):
raise TypeError(
f"Argument '{param_name}': expected {hints[param_name].__name__}, "
f"got {type(value).__name__}"
)
for key, value in kwargs.items():
if key in hints and not isinstance(value, hints[key]):
raise TypeError(
f"Argument '{key}': expected {hints[key].__name__}, "
f"got {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
@validate_types
def greet(name: str, age: int) -> str:
return f"{name} is {age} years old."
print(greet("Alice", 30)) # OK
# Alice is 30 years old.
# greet("Alice", "thirty")
# TypeError: Argument 'age': expected int, got str
知识检查
-
装饰器
@decorator放在函数定义上方时,等价于以下哪种写法?- A.
decorator(func) - B.
func = decorator(func) - C.
func(decorator) - D.
decorator = func
- A.
-
functools.wraps的主要作用是?- A. 加速函数执行
- B. 自动注册函数到某个注册表
- C. 保留被装饰函数的元数据(
__name__、__doc__等) - D. 为函数添加参数类型检查
-
当使用
@A @B def f(): pass时,函数的执行顺序是?- A. A 的 wrapper 先执行,B 的 wrapper 再执行,最后执行原始 f
- B. B 的 wrapper 先执行,A 的 wrapper 再执行,最后执行原始 f
- C. 原始 f 先执行,然后 B,最后 A
- D. A 和 B 并行执行
查看答案
- B —
@decorator等价于函数定义后执行func = decorator(func) - C —
wraps将原始函数的__name__、__doc__、__module__、__qualname__、__annotations__、__dict__、__wrapped__复制到 wrapper 上 - A —
@A @B def f()等价于f = A(B(f)):B 先包裹 f,A 再包裹 B 的结果;调用时 A 的 wrapper 最外层先执行,进入后 B 的 wrapper 执行,最后才执行原始 f
本章小结
- 装饰器是接收函数并返回函数的高阶函数,核心前提是一等函数(函数可赋值、可传递、可返回)
@decorator语法糖等价于func = decorator(func),定义后立即生效functools.wraps必须用于 wrapper 函数,以保留原始函数的元数据,避免调试和框架集成问题- 带参数的装饰器需要三层嵌套:参数接收层 → 装饰器函数层 → wrapper 执行层
- 多个装饰器可以叠加(stack),执行遵循"包裹顺序从下往上、调用顺序从外到内"的规则
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| decorator | 装饰器 | 接收函数并返回函数的高阶函数,用于增强函数行为 |
| first-class function | 一等函数 | 在 Python 中,函数可以像普通变量一样被赋值、传递、作为参数或返回值 |
| wrapper function | 包装函数 | 装饰器内部定义的函数,包裹原始函数并添加额外逻辑 |
functools.wraps | wraps 装饰器 | 将原始函数元数据复制到 wrapper 上的实用装饰器 |
| closure | 闭包 | 内层函数捕获外层函数变量的现象,是装饰器能记住被装饰函数的关键机制 |
*args / **kwargs | 可变参数 | 用于将任意位置参数和关键字参数转发给原始函数的语法 |
| decorator factory | 装饰工厂 | 返回装饰器函数的函数,用于创建带参数的装饰器 |
| stacked decorators | 叠加装饰器 | 多个装饰器同时修饰一个函数,从下往上执行包裹 |
下一步
- 生成器 (Generators) → 学习
yield和迭代器协议,构建惰性计算管道
源码链接
生成器与迭代 (Generators & Iteration)
导语
假设你需要处理一个包含 10 亿行日志的文件。如果用列表把所有行读入内存,你的程序会瞬间耗尽几 GB 内存然后崩溃。但如果逐行读取、处理完就丢弃,内存占用几乎为零——这就是懒加载(lazy evaluation)的力量。Python 的生成器正是实现懒加载的核心工具:它每次只生成一个值,用多少算多少,特别适合处理大数据流、无限序列和管道式数据处理。掌握生成器,你的程序既省内存又高效。
学习目标
- 理解迭代器协议(
__iter__/__next__)和生成器函数的区别与联系 - 掌握
yield关键字的工作原理、生成器表达式以及yield from的用法 - 学会使用
.send()、.throw()、.close()实现生成器的双向通信
概念介绍
生成器是 Python 中一种特殊的迭代器,它通过 yield 关键字实现按需生产值。理解生成器需要先理解迭代器协议。
几个关键概念:
- 迭代器协议(Iterator Protocol) — 任何实现了
__iter__()和__next__()方法的对象都是迭代器。__iter__()返回迭代器自身,__next__()返回下一个值,没有更多值时抛出StopIteration。for循环底层就是通过这个协议工作的。 - 生成器函数(Generator Function) — 用
yield而非return返回值的函数。每次调用next()时,生成器函数从上次暂停的位置继续执行,保留了所有局部变量和调用栈。这种「暂停-恢复」机制使其内存效率极高。 - 生成器表达式(Generator Expression) — 类似列表推导式的语法,但用圆括号而非方括号。例如
(x**2 for x in range(1000000))不创建完整列表,而是在需要时逐个产生值。 yield from— 将控制权委托给另一个生成器或可迭代对象。它等价于用for循环逐一yield子 iterable 的所有元素,但代码更简洁,且正确处理了send()/throw()等双向通信。- 双向通信(Two-way Communication) — 生成器不仅是生产者,也是消费者。
.send(value)可以向生成器内部发送值(作为yield表达式的返回值),.throw()可以在生成器内部抛入异常,.close()可以提前关闭生成器。
tip
判断一个对象是否可以迭代:只要它能用于 for item in obj 或传给 iter(obj) 而不报错,它就是可迭代的。生成器是迭代器的一种,所有生成器都是可迭代的。
代码示例
示例 1:基本生成器函数 — 斐波那契数列
def fibonacci():
"""生成斐波那契数列的无限生成器。"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
def main():
# 创建生成器对象
fib = fibonacci()
# 使用 next() 逐个获取值
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3
# 更常见的用法:结合 break 取前 N 项
print("\nFirst 10 Fibonacci numbers:")
for i, num in enumerate(fibonacci()):
if i >= 10:
break
print(num, end=" ")
print()
if __name__ == "__main__":
main()
斐波那契数列是无限序列,用列表存储会无限增长。生成器让它成为可能——每次只计算并返回下一个值,内存中始终只保存 a 和 b 两个变量。注意第二次 for 循环创建了一个新的生成器实例,因为 fibonacci() 每次调用都返回独立的生成器对象。
important
生成器是一次性的。一旦迭代耗尽(抛出 StopIteration),无法重新迭代。如需重用,必须重新调用生成器函数创建新实例。
示例 2:生成器表达式 vs 列表推导式 — 内存对比
import sys
def compare_memory():
"""对比列表推导式和生成器表达式的内存占用。"""
# 列表推导式 — 一次性创建所有元素
list_comp = [x ** 2 for x in range(1000000)]
print(f"List comprehension: {sys.getsizeof(list_comp):,} bytes")
# List comprehension: 8,906,504 bytes (~8.9 MB)
# 生成器表达式 — 不创建列表,只创建生成器对象
gen_exp = (x ** 2 for x in range(1000000))
print(f"Generator expression: {sys.getsizeof(gen_exp):,} bytes")
# Generator expression: 112 bytes
# 验证生成器的值与列表一致
squares = list(gen_exp)
print(f"Generated {len(squares)} squares")
print(f"First 5: {squares[:5]}")
print(f"Last 5: {squares[-5:]}")
def main():
compare_memory()
if __name__ == "__main__":
main()
生成器表达式只占用约 100 字节,而列表推导式需要近 9 MB。当数据量增大到 1 亿条时,列表方式会耗尽内存,而生成器依然只占 100 多字节。生成器的核心优势在于空间复杂度从 O(N) 降到 O(1)。
tip
什么时候用列表推导式,什么时候用生成器表达式?答案是:如果只需要遍历一次(如传给 sum()、max()、list() 或用在 for 循环中),用生成器表达式;如果需要多次访问、索引或切片,用列表推导式。
示例 3:yield from 与双向通信 — .send()`` 和 .close()`
def running_average():
"""计算运行平均值的生成器,演示 send/close 双向通信。"""
total = 0
count = 0
average = 0
while True:
value = yield average
if value is None:
break
total += value
count += 1
average = total / count
def flatten_nested(nested_list):
"""使用 yield from 展平嵌套列表。"""
for item in nested_list:
if isinstance(item, list):
yield from flatten_nested(item)
else:
yield item
def main():
print("--- Running Average with .send() ---")
avg_gen = running_average()
# 第一次必须用 next() 启动生成器,运行到第一个 yield
initial = next(avg_gen)
print(f"Initial average: {initial}") # 0.0
# 使用 send 发送值并获取新的平均值
print(f"After 10: {avg_gen.send(10)}") # 10.0
print(f"After 20: {avg_gen.send(20)}") # 15.0
print(f"After 30: {avg_gen.send(30)}") # 20.0
print(f"After 40: {avg_gen.send(40)}") # 25.0
# 用 close() 关闭生成器
avg_gen.close()
print("Generator closed.")
print("\n--- yield from: Flattening Nested Lists ---")
nested = [[1, 2], [3, [4, 5]], [6]]
flat = list(flatten_nested(nested))
print(f"Flattened: {flat}")
# Flattened: [1, 2, 3, 4, 5, 6]
if __name__ == "__main__":
main()
运行平均值生成器展示了双向通信的完整流程:next() 首次启动生成器并获取初始值,之后 .send(value) 将值传入生成器内部(作为 yield 表达式的返回值),同时获取新的计算结果。.close() 会向生成器内部抛出 GeneratorExit,安全终止生成器。
yield from 则是 Python 3.3 引入的语法糖,它将迭代任务委托给子可迭代对象,自动处理所有元素的逐一产出,包括递归结构的展平。相比手动写 for ... yield ...,代码更简洁且行为更正确。
常见错误与解决
warning
错误 1:重复使用耗尽的生成器
def count_to_3():
for i in range(1, 4):
yield i
gen = count_to_3()
print(list(gen)) # [1, 2, 3]
print(list(gen)) # 💥 [] — 生成器已耗尽!
原因:生成器是一次性使用的迭代器。一旦迭代到末尾(抛出 StopIteration),所有后续迭代都直接返回空结果。
解决:重新调用生成器函数创建新实例。
gen = count_to_3()
print(list(gen)) # [1, 2, 3]
gen = count_to_3() # 创建新实例
print(list(gen)) # ✅ [1, 2, 3]
warning
错误 2:自定义迭代器忘记在 __next__ 中抛出 StopIteration
class BadCounter:
def __init__(self, limit):
self.limit = limit
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current < self.limit:
self.current += 1
return self.current
# 💥 忘记 return 或抛异常!Python 会隐式返回 None,导致 for 循环异常
for x in BadCounter(3):
print(x) # 抛出 TypeError 或死循环
原因:迭代协议要求 __next__ 在耗尽时必须抛出 StopIteration。不抛出则 for 循环无法正确终止。
解决:明确抛出 StopIteration。
def __next__(self):
if self.current < self.limit:
self.current += 1
return self.current
raise StopIteration # ✅ 正确
最佳实践
- 大数据处理优先用生成器 — 读取大文件、处理流式数据时,用生成器逐条处理而非一次性加载,内存占用稳定在 O(1)
- 用
yield from替代嵌套的for-yield—yield from sub_generator比for item in sub_generator: yield item更简洁,且正确传递send()/throw()信号 - 生成器函数内不要混用
return value— PEP 380 规定生成器中的return value等价于raise StopIteration(value),但容易引起误解。需要终止时直接用return或raise StopIteration
练习
- 编写一个生成器函数
chunked(iterable, size),将可迭代对象按指定大小分块,每块返回一个列表。
查看答案
def chunked(iterable, size):
"""将可迭代对象按指定大小分块。"""
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == size:
yield chunk
chunk = []
if chunk:
yield chunk
# 使用示例
data = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for batch in chunked(data, 3):
print(batch)
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
- 编写一个生成器
prime_generator(),按需生成素数(无限序列),并用它打印前 10 个素数。
查看答案
def prime_generator():
"""按需生成素数的无限生成器。"""
yield 2
candidate = 3
while True:
is_prime = True
for i in range(2, int(candidate ** 0.5) + 1):
if candidate % i == 0:
is_prime = False
break
if is_prime:
yield candidate
candidate += 2
# 打印前 10 个素数
count = 0
for prime in prime_generator():
if count >= 10:
break
print(prime, end=" ")
count += 1
# 2 3 5 7 11 13 17 19 23 29
知识检查
-
生成器表达式
(x for x in range(5))与列表推导式[x for x in range(5)]的主要区别是?- A. 语法不同,功能完全一样
- B. 生成器表达式延迟求值,列表推导式立即求值
- C. 生成器表达式支持索引访问
- D. 列表推导式内存效率更高
-
以下哪个方法可以将值发送到生成器内部?
- A.
gen.push(value) - B.
gen.send(value) - C.
gen.write(value) - D.
gen.input(value)
- A.
-
yield from [1, 2, 3]等价于以下哪段代码?- A.
yield [1, 2, 3] - B.
yield 1, 2, 3 - C.
for x in [1, 2, 3]: yield x - D.
return [1, 2, 3]
- A.
查看答案
- B — 生成器表达式只在迭代时逐个产生值(懒加载),列表推导式立即创建完整列表
- B —
.send(value)是生成器的内置方法,用于向内部发送值并获取下一个产出值 - C —
yield from将子可迭代对象的每个元素逐一产出,等价于 for 循环 + yield
本章小结
- 迭代器协议由
__iter__()和__next__()组成,for循环通过该协议工作 - 生成器函数使用
yield实现暂停-恢复机制,每次只生成一个值,内存效率极高 - 生成器表达式
(expr for var in iterable)语法与列表推导式相同但延迟求值,空间复杂度 O(1) yield from将迭代任务委托给子可迭代对象,支持递归结构和双向通信- 生成器是一次性的,耗尽后需重新创建;
.send()和.close()实现生成器的双向控制
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| generator | 生成器 | 使用 yield 的函数,按需产生值而非一次性返回所有结果 |
| iterator protocol | 迭代器协议 | 由 __iter__() 和 __next__() 组成的接口规范 |
| yield | 产出关键字 | 暂停函数执行并返回当前值,下次调用时从该位置恢复 |
| lazy evaluation | 懒加载/延迟求值 | 只在需要时才计算值,而非提前计算所有结果 |
| generator expression | 生成器表达式 | 用圆括号的推导式语法,返回生成器而非列表 |
| yield from | 委托产出 | 将迭代任务委托给子可迭代对象的语法糖 |
| coroutine | 协程 | 可通过 .send() 接收外部值的生成器,支持双向通信 |
| StopIteration | 迭代终止异常 | 迭代器耗尽时抛出的异常,for 循环捕获后会正常终止 |
下一步
- 上下文管理器 → 学习
with语句和__enter__/__exit__协议
源码链接
- 本章无对应源码文件 — 示例代码可直接在文档中运行
上下文管理器 (Context Managers)
导语
想象你去自助餐厅吃饭:拿起餐盘→取菜→用餐→归还餐盘。这个流程中,"归还餐盘"不能忘,否则餐厅会多收押金。Python 编程中也经常遇到类似场景:打开文件后必须关闭、获取锁后必须释放、建立连接后必须断开。上下文管理器(Context Manager)就是 Python 提供的"自动归还餐盘"机制——它确保资源在使用完毕后一定被正确清理,即使中间发生异常也不会遗漏。with 语句是上下文管理器的语法入口,理解它不仅能写出更安全的代码,还能让你自定义优雅的资源管理工具。
学习目标
- 掌握
with语句的工作机制和__enter__/__exit__协议 - 学会使用
@contextmanager装饰器以生成器方式定义上下文管理器 - 掌握
contextlib.suppress、contextlib.redirect_stdout等实用工具
概念介绍
上下文管理器的核心是资源生命周期管理——在进入代码块时获取资源,在离开代码块时释放资源。Python 通过 with 语句和上下文管理器协议来实现这一模式。
with语句 — 语法糖,底层调用上下文管理器的__enter__()获取资源,代码块执行完毕后(无论正常结束还是异常退出)调用__exit__()释放资源。__enter__和__exit__协议 — 上下文管理器类必须实现这两个魔术方法。__enter__()在进入with块时调用,返回值可通过as绑定;__exit__(exc_type, exc_val, exc_tb)在离开时调用,三个参数分别描述异常的类型、值和追踪信息,如果没有异常则全部为None。contextlib.contextmanager装饰器 — 用生成器(yield)替代类来定义上下文管理器。yield之前的代码相当于__enter__,yield之后的代码相当于__exit__。适合简单场景,代码更简洁。contextlib.suppress— 忽略指定异常类型的上下文管理器,等价于try-except但更简洁。- 嵌套上下文管理器 —
with语句支持嵌套(或使用同一行多对象),内层资源在外层之前释放,遵循"后分配先释放"的栈式顺序。
note
Python 内置的 open() 函数返回的文件对象就是一个上下文管理器——这就是为什么推荐用 with open(...) as f 而非手动 f.close()。
代码示例
示例 1:自定义上下文管理器类 — Timer
import time
class Timer:
"""A context manager that measures execution time."""
def __enter__(self):
self.start = time.perf_counter()
print("Timer started.")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.perf_counter()
self.elapsed = self.end - self.start
print(f"Timer stopped. Elapsed: {self.elapsed:.4f} seconds")
return False # Do not suppress exceptions
# Usage
with Timer() as t:
time.sleep(0.5)
print(f"Inside: elapsed so far = {time.perf_counter() - t.start:.4f}s")
# 输出:
# Timer started.
# Inside: elapsed so far = 0.5012s
# Timer stopped. Elapsed: 0.5034s
__enter__() 返回 self,使得 as t 能访问 Timer 实例的属性(如 elapsed)。__exit__() 返回 False 表示不吞掉异常——如果 with 块内发生异常,异常会继续向外传播。
tip
time.perf_counter() 比 time.time() 更精确,专用于测量短时间间隔,不受系统时钟更新影响。
示例 2:@contextmanager 装饰器 — 临时工作目录
import os
from contextlib import contextmanager
@contextmanager
def temporary_dir():
"""Create a temp directory and clean up on exit."""
original_cwd = os.getcwd()
temp_path = "tmp_temp_workspace"
os.makedirs(temp_path, exist_ok=True)
os.chdir(temp_path)
print(f"Changed to: {os.getcwd()}")
try:
yield temp_path
finally:
os.chdir(original_cwd)
os.rmdir(temp_path)
print(f"Restored to: {os.getcwd()}")
# Usage
print(f"Before: {os.getcwd()}")
with temporary_dir() as td:
print(f"Inside: creating file in {td}")
with open("temp_data.txt", "w") as f:
f.write("temporary data")
print(f"After: {os.getcwd()}")
# 输出:
# Before: /path/to/project
# Changed to: /path/to/project/tmp_temp_workspace
# Inside: creating file in tmp_temp_workspace
# Restored to: /path/to/project
# After: /path/to/project
@contextmanager 将生成器函数转换为上下文管理器。yield 之前的代码在 __enter__ 阶段执行,yield 的值通过 as 绑定,yield 之后的代码在 __exit__ 阶段(放在 finally 中确保即使异常也执行)。
warning
使用 @contextmanager 时,必须用 try...finally 包裹清理逻辑。如果忘记 finally,当 with 块内抛出异常时,清理代码可能不会执行,导致资源泄漏。
示例 3:contextlib.suppress 和 contextlib.redirect_stdout
import os
from contextlib import suppress, redirect_stdout
from io import StringIO
# suppress: ignore specific exceptions
print("--- suppress example ---")
with suppress(FileNotFoundError):
os.remove("non_existent_file.txt")
print("No error raised, program continues.")
print("")
# redirect_stdout: capture stdout output
print("--- redirect_stdout example ---")
buffer = StringIO()
with redirect_stdout(buffer):
print("Hello, captured world!")
print("This goes to buffer, not console.")
captured = buffer.getvalue()
print(f"Captured output: {captured.strip()}")
# 输出:
# --- suppress example ---
# No error raised, program continues.
#
# --- redirect_stdout example ---
# Captured output: Hello, captured world!
# This goes to buffer, not console.
suppress(FileNotFoundError) 等价于:
try:
os.remove("non_existent_file.txt")
except FileNotFoundError:
pass
当需要忽略一两个特定异常时,suppress 比 try-except 更声明式。
redirect_stdout(f) 将 print() 等写入 sys.stdout 的输出重定向到文件对象 f。配合 StringIO 可以捕获输出用于测试验证。
note
suppress() 也可以传入多个异常类型:suppress(FileNotFoundError, PermissionError)。
常见错误与解决
warning
错误 1:在 __exit__ 中吞掉异常而不传播
class BadManager:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Cleanup done")
return True # 💥 所有异常都被吞掉了!
with BadManager():
raise ValueError("Something went wrong")
print("This prints — exception was silently swallowed!")
原因:__exit__() 返回 True 表示"异常已处理",会阻止异常继续传播。除非你明确想要忽略异常,否则不应返回 True。
解决:默认返回 False,或者不写 return(Python 默认返回 None,等价于 False)。仅在 exc_type is not None 且确实需要时返回 True。
warning
错误 2:在 @contextmanager 中遗漏 finally 导致异常时资源未释放
from contextlib import contextmanager
@contextmanager
def leaky_resource():
resource = open("data.txt", "w") # acquired
yield resource
resource.close() # 💥 如果 with 块内抛异常,这行不会执行!
原因:当 with 块内抛出异常时,生成器在 yield 处被中断,yield 之后的清理代码不会执行。
解决:始终用 try...finally 包裹:
@contextmanager
def safe_resource():
resource = open("data.txt", "w")
try:
yield resource
finally:
resource.close() # ✅ 无论如何都会执行
最佳实践
- 优先使用
with语句管理资源 — 文件、网络连接、锁等同步资源都应用with语句包裹,即使对象本身支持手动 close/释放,with也能保证异常路径下的正确清理 - 简单场景用
@contextmanager,复杂场景用类 — 如果上下文管理器需要维护多个状态、或需要除with之外调用方法(如示例 1 中的.elapsed),用类实现;如果只是清理一个资源,@contextmanager代码更简洁 __exit__中不要吞掉非预期异常 —__exit__()默认返回False,仅在明确需要忽略特定异常时返回True,并在日志中记录被忽略的异常信息
练习
- 实现一个
Transaction上下文管理器,模拟数据库事务的提交与回滚。with块正常结束时打印"Transaction committed.",抛出异常时打印"Transaction rolled back."并重新抛出异常。
查看答案
class Transaction:
def __enter__(self):
print("Transaction started.")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
print("Transaction committed.")
else:
print("Transaction rolled back.")
return False
with Transaction():
print("Doing work...")
raise RuntimeError("Something failed")
# 输出:
# Transaction started.
# Doing work...
# Transaction rolled back.
# (RuntimeError propagated)
- 使用
@contextmanager实现一个changed_env(key, value)上下文管理器,在with块内临时修改环境变量,离开后自动恢复原值。
查看答案
import os
from contextlib import contextmanager
@contextmanager
def changed_env(key, value):
original = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
if original is None:
os.environ.pop(key, None)
else:
os.environ[key] = original
print(f"Before: {os.environ.get('MY_VAR', 'NOT_SET')}")
with changed_env("MY_VAR", "temporary"):
print(f"Inside: {os.environ['MY_VAR']}")
print(f"After: {os.environ.get('MY_VAR', 'NOT_SET')}")
# 输出:
# Before: NOT_SET
# Inside: temporary
# After: NOT_SET
知识检查
-
__exit__(exc_type, exc_val, exc_tb)返回True的效果是什么?- A. 重新抛出异常
- B. 吞掉异常,阻止其继续传播
- C. 忽略返回值,不影响异常流
- D. 触发二次异常
-
@contextmanager装饰器要求被装饰函数是什么类型?- A. 普通函数(返回 None)
- B. 异步函数(
async def) - C. 生成器函数(包含
yield) - D. Lambda 函数
-
contextlib.suppress(ValueError)等价于以下哪种写法?- A.
try: ... except Exception: pass - B.
try: ... except ValueError: pass - C.
try: ... finally: pass - D.
if ValueError: continue
- A.
查看答案
- B — 返回
True会让 Python 认为异常已被处理,不再向外传播 - C —
@contextmanager将包含yield的生成器函数转换为上下文管理器 - B —
suppress(ValueError)只忽略ValueError及其子类,其他异常正常传播
本章小结
with语句通过调用__enter__()和__exit__()实现自动资源管理,即使异常也不会泄漏资源__exit__()接收异常三要素(exc_type, exc_val, exc_tb),返回False(默认)让异常继续传播,返回True吞掉异常@contextmanager用生成器 +yield简化上下文管理器定义,但清理逻辑必须放在try...finally中contextlib.suppress()优雅地忽略指定异常,比try-except-pass更声明式contextlib.redirect_stdout()可将print()输出重定向到任意文件对象,常用于测试和日志
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| context manager | 上下文管理器 | 实现 __enter__/__exit__ 协议、用于资源管理的对象 |
with statement | with 语句 | Python 中用于自动管理资源的语法结构 |
__enter__ | 进入方法 | 上下文管理器在进入 with 块时调用的方法 |
__exit__ | 退出方法 | 上下文管理器在离开 with 块时调用的方法,负责清理资源 |
@contextmanager | 上下文装饰器 | contextlib 模块提供的装饰器,用生成器定义上下文管理器 |
contextlib.suppress | 异常抑制器 | 忽略指定异常类型而不传播的上下文管理器 |
下一步
- 类型注解 (Type Hints) → 学习 Python 的类型提示系统,让代码更可读、更可维护
源码链接
- 本章暂无对应的独立源码文件
类型提示 (Type Hints)
导语
Python 是一门动态类型语言——变量不需要声明类型,同一个变量可以在运行时改变类型。这种灵活性带来便利的同时,也让大型项目的代码维护和团队协作变得困难。类型提示(Type Hints)是 Python 3.5 引入的可选语法,允许你为变量、函数参数和返回值标注期望的类型。IDE 可以据此提供智能补全,mypy 等静态检查工具可以在运行前发现类型错误,代码阅读者也能一目了然地理解数据流向。Python 的渐进式类型(gradual typing)哲学让类型提示既强大又灵活——你可以从零开始,逐步为项目添加类型标注。
学习目标
- 掌握函数参数和返回值的类型注解语法
- 熟练使用
typing模块中的List、Dict、Optional、Union、TypeAlias等类型工具 - 理解 Python 3.10+ 的
X | Y联合类型语法、TypedDict和Protocol的高级用法
概念介绍
类型提示是 Python 中的可选类型标注系统,核心设计理念是渐进式类型(gradual typing)——你可以在现有代码中逐步添加类型注解,而不影响运行行为。Python 解释器在运行时完全忽略类型提示,它们仅服务于静态分析工具(mypy、pyright)和 IDE(自动补全、重构)。
- 基本类型注解 — 函数参数使用
name: type语法,返回值使用-> type语法。例如def greet(name: str) -> str:。变量注解使用var: int = 0。 typing模块 — Python 标准库提供List、Dict、Set、Tuple等泛型容器类型。Optional[X]等价于X | None,Union[X, Y]表示"X 或 Y"。X | Y联合语法 — Python 3.10 引入的管道符联合类型,比Union[X, Y]更简洁。str | None比Optional[str]更直观。TypeAlias— 为复杂类型创建可读的别名,提高代码可维护性。TypedDict— 为字典定义结构化键值类型,比纯Dict[str, Any]更精准。Protocol— Python 的"结构子类型"(structural subtyping)——定义接口契约,只要对象实现了所需的方法和属性,就视为兼容,不需要显式继承。- mypy — 最流行的 Python 静态类型检查器。安装后对文件运行
mypy myfile.py,即可在运行前捕获类型错误。
tip
Python 3.10 的 X | Y 语法已是官方推荐写法,新项目建议优先使用 str | None 而非 Optional[str],int | str 而非 Union[int, str]。
代码示例
Example 1: Basic function annotations + return types
from typing import Optional
def greet(name: str) -> str:
"""Return a greeting string for the given name."""
return f"Hello, {name}!"
def calculate_total(price: float, quantity: int, discount: Optional[float] = None) -> float:
"""Calculate the total price with an optional discount."""
total = price * quantity
if discount is not None:
total *= (1 - discount)
return round(total, 2)
def find_user(users: list[tuple[int, str]], user_id: int) -> str | None:
"""Find a user name by ID. Returns None if not found."""
for uid, name in users:
if uid == user_id:
return name
return None
if __name__ == "__main__":
print(greet("Alice")) # Hello, Alice!
result = calculate_total(9.99, 3, 0.1)
print(f"Total: {result}") # Total: 26.97
user_list: list[tuple[int, str]] = [(1, "Alice"), (2, "Bob")]
found = find_user(user_list, 2)
print(f"Found: {found}") # Found: Bob
note
函数注解可通过 greet.__annotations__ 在运行时访问:{'name': <class 'str'>, 'return': <class 'str'>}。但 Python 运行时不会检查传入值是否匹配注解的类型。
Example 2: Complex types (Optional, Union, list[dict[str, int]]) and X | Y syntax
from typing import TypeAlias
# Python 3.10+ union syntax
def process_value(value: str | int | float) -> str:
"""Process a value that can be string, int, or float."""
match value:
case str():
return f"String: {value.upper()}"
case int() | float():
return f"Number: {value * 2}"
# TypeAlias for complex nested types
ScoreBoard: TypeAlias = dict[str, list[int]]
def get_top_student(scores: ScoreBoard) -> str | None:
"""Find the student with the highest average score."""
if not scores:
return None
averages = {name: sum(s) / len(s) for name, s in scores.items()}
top = max(averages, key=averages.get)
return top
UserRecord: TypeAlias = dict[str, str | int | bool]
def format_user(record: UserRecord) -> str:
"""Format a user record into a readable string."""
name: str = record.get("name", "Unknown")
age: int = record.get("age", 0)
active: bool = record.get("active", False)
status = "active" if active else "inactive"
return f"{name} (age {age}, {status})"
if __name__ == "__main__":
print(process_value("hello")) # String: HELLO
print(process_value(42)) # Number: 84
scores: ScoreBoard = {
"Alice": [95, 87, 92],
"Bob": [80, 78, 85],
"Charlie": [90, 93, 91],
}
print(f"Top: {get_top_student(scores)}") # Top: Charlie
user: UserRecord = {"name": "Alice", "age": 30, "active": True}
print(format_user(user)) # Alice (age 30, active)
tip
TypeAlias 不会创建新类型——它仅为复杂的类型表达式赋予可读的名称。mypy 仍然会按原类型严格检查。
Example 3: TypedDict and Protocol (structural subtyping)
from typing import Protocol, TypedDict
class UserDict(TypedDict):
"""Typed dictionary for user data."""
name: str
age: int
email: str
def create_user(name: str, age: int, email: str) -> UserDict:
"""Create a typed user dictionary."""
return {"name": name, "age": age, "email": email}
def describe_user(user: UserDict) -> str:
"""Describe a user from a typed dictionary."""
return f"{user['name']}, age {user['age']}, email {user['email']}"
class Serializable(Protocol):
"""Protocol for objects that can be serialized to string."""
def serialize(self) -> str: ...
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def serialize(self) -> str:
return f"User({self.name}, {self.age})"
class Config:
def __init__(self, data: dict):
self.data = data
def serialize(self) -> str:
return str(self.data)
def serialize_any(obj: Serializable) -> str:
"""Accept any object that follows the Serializable protocol."""
return obj.serialize()
if __name__ == "__main__":
# TypedDict usage
user = create_user("Alice", 30, "alice@example.com")
print(describe_user(user)) # Alice, age 30, email alice@example.com
# Protocol-based structural subtyping
u = User("Bob", 25)
c = Config({"debug": True})
print(serialize_any(u)) # User(Bob, 25)
print(serialize_any(c)) # {'debug': True}
note
Protocol 实现的是结构子类型——User 和 Config 不需要声明继承 Serializable,只要它们有 serialize() -> str 方法,就被认为兼容 Serializable 协议。这与 Java/C# 的接口(名义子类型)不同。
常见错误与解决
warning
错误 1:混淆运行时检查与静态检查
def add(a: int, b: int) -> int:
return a + b
>
result = add("hello", "world") # 运行时不会报错!Python 不检查类型
print(result) # "helloworld" — 字符串拼接
原因:类型提示在运行时完全被忽略。add("hello", "world") 不会抛出任何异常,因为 Python 解释器不执行类型检查。
解决:在 CI/CD 流程中集成 mypy,在代码合并前自动检测类型错误。
pip install mypy
mypy myfile.py # 会报告 add("hello", "world") 类型不匹配
warning
错误 2:过度注解(Over-annotating trivial code)
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
>
x: int = 1
y: int = 2
z: int = add(x, y)
原因:对于简单赋值和推导,Python 可以自行推断类型。过度注解会增加代码噪音,降低可读性。
解决:只为函数签名(参数和返回值)和复杂类型添加注解。简单变量的赋值不需要显式注解。
def add(a: int, b: int) -> int:
return a + b
>
x = 1 # inference: int
z = add(x, 2) # inference: int
最佳实践
- 优先注解函数签名 — 函数参数和返回值是类型提示的核心价值所在。让 IDE 和 mypy 在函数调用点捕获错误,远比注解局部变量重要
- Python 3.10+ 使用
X | Y而非Union[X, Y]—|语法更简洁,是 PEP 604 的推荐写法。Optional[X]可读性好可以保留,但X | None也完全可以 - 在 CI 中集成 mypy — 类型提示的价值在于工具链。在 GitHub Actions 中添加
mypy检查步骤,确保类型注解不会被破坏
练习
- 使用
TypedDict定义一个Product类型,包含name: str、price: float、tags: list[str]。编写函数discount(product: Product, rate: float) -> float,返回打折后的价格。
查看答案
from typing import TypedDict
class Product(TypedDict):
name: str
price: float
tags: list[str]
def discount(product: Product, rate: float) -> float:
"""Return the discounted price."""
return round(product["price"] * (1 - rate), 2)
if __name__ == "__main__":
p: Product = {"name": "Laptop", "price": 999.99, "tags": ["electronics", "computer"]}
print(discount(p, 0.2)) # 799.99
- 定义
Protocol名为Drawable,包含方法draw(self) -> str。创建Circle和Square两个类实现它,编写函数render(shape: Drawable) -> str调用shape.draw()并返回结果。
查看答案
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "Drawing a circle"
class Square:
def draw(self) -> str:
return "Drawing a square"
def render(shape: Drawable) -> str:
return shape.draw()
if __name__ == "__main__":
print(render(Circle())) # Drawing a circle
print(render(Square())) # Drawing a square
知识检查
-
Python 的 type hints 在运行时会被执行吗?
- A. 会,Python 会自动检查传入参数的类型
- B. 不会,Python 解释器在运行时完全忽略类型提示
- C. 仅在函数首次调用时检查一次
- D. 取决于是否安装了 mypy
-
Python 3.10+ 中,
str | None等价于typing模块中的哪个类型?- A.
Union[str, None] - B.
Optional[str] - C. 两者都等价
- D. 没有等价类型
- A.
-
TypedDict和Protocol的核心区别是?- A.
TypedDict定义字典的键值类型,Protocol定义对象的行为接口 - B.
TypedDict用于类,Protocol用于字典 - C.
TypedDict在运行时被检查,Protocol不被检查 - D. 没有区别,可以互换使用
- A.
查看答案
- B — Python 解释器运行时不执行类型检查,类型提示仅服务于静态分析工具和 IDE
- C —
Optional[str]等价于Union[str, None],也等价于str | None,三者语义相同 - A —
TypedDict描述字典内部结构(键和对应的值类型),Protocol描述对象的行为契约(方法和属性)
本章小结
- 类型提示是 Python 的可选语法,运行时不执行,由 mypy 等静态工具在运行前检查
- 函数参数使用
name: type语法,返回值使用-> type语法 typing模块提供List、Dict、Optional、Union、TypeAlias、TypedDict、Protocol等丰富工具- Python 3.10+ 的
X | Y联合类型语法是官方推荐写法,更简洁直观 - 类型提示的价值依赖于工具链——IDE 自动补全、mypy 静态检查、CI 集成缺一不可
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| type hint | 类型提示 | Python 中为变量和函数标注期望类型的可选语法 |
| gradual typing | 渐进式类型 | 允许在动态类型语言中逐步添加类型标注的理念 |
| mypy | 静态类型检查器 | 检查 Python 代码类型一致性的工具 |
| Union | 联合类型 | 表示一个值可以是多种类型之一 |
| Optional | 可选类型 | 等价于 `X |
| TypeAlias | 类型别名 | 为复杂类型表达式创建可读名称 |
| TypedDict | 类型化字典 | 定义字典的键和对应值类型的结构化标注 |
| Protocol | 协议 | Python 的结构子类型机制,定义行为接口契约 |
| structural subtyping | 结构子类型 | 基于对象拥有的方法和属性判断类型兼容性,而非继承关系 |
下一步
- Dataclasses 和 Pydantic → 学习现代 Python 数据建模工具
源码链接
数据类 (Data Classes)
导语
在 Python 中,我们常常需要创建仅用于存储数据的类——比如表示坐标的点、配置参数、API 响应模型等。一个典型的"数据容器"类往往充斥着重复的样板代码:__init__ 赋值、__repr__ 调试输出、__eq__ 相等比较……这些工作乏味且容易出错。Python 3.7 引入的 dataclasses 模块通过 @dataclass 装饰器自动生成这些样板方法,让你用最少代码声明数据结构。数据类在概念上类似命名元组(namedtuple),但更灵活——支持类型注解、默认值、继承和自定义初始化逻辑,是现代 Python 项目中构建数据载体的首选方式。
学习目标
- 掌握
@dataclass装饰器的基本用法,理解自动生成的__init__、__repr__、__eq__方法 - 学会使用
field()配置默认值、default_factory和frozen=True不可变语义 - 理解
__post_init__钩子和数据类继承的用法与限制
概念介绍
dataclasses 模块的核心是 @dataclass 装饰器。它扫描类体中带类型注解的字段,自动生成一系列特殊方法:
__init__— 根据字段声明生成构造函数,支持默认值__repr__— 生成可读的字符串表示,格式为ClassName(field1=value1, field2=value2)__eq__— 按字段值逐个比较,判断两个实例是否相等__lt__、__le__、__gt__、__ge__— 比较运算(需设置order=True)__hash__— 哈希值(仅当frozen=True或unsafe_hash=True时生成)
field() 函数提供字段的细粒度控制,常用参数包括:default(默认值)、default_factory(无参工厂函数,生成可变默认值)、init=False(不参与构造函数)、repr=False(不在 __repr__ 中展示)、frozen=True(该字段不可变)、compare=False(不参与 __eq__ 比较)等。
tip
数据类与命名元组(namedtuple)的关系:namedtuple 是不可变的 tuple 子类,侧重节省内存和向后兼容;dataclass 是普通 class,支持默认值、修改字段、继承和 __post_init__,适用范围更广。两者都可用 _asdict() / dataclasses.asdict() 转为字典。
代码示例
示例 1:基本 @dataclass — 自动生成常用方法
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Person:
name: str
age: int
email: str = ""
# Auto-generated __init__: constructor with typed parameters
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
p3 = Point(1.0, 2.0)
# Auto-generated __repr__: clean debug-friendly string representation
print(p1) # Point(x=3.0, y=4.0)
# Auto-generated __eq__: field-by-field value comparison
print(p1 == p2) # True
print(p1 == p3) # False
# Person with optional default value
alice = Person("Alice", 30)
bob = Person("Bob", 25, "bob@example.com")
print(alice) # Person(name='Alice', age=30, email='')
print(bob) # Person(name='Bob', age=25, email='bob@example.com')
# Mutability: fields can be reassigned by default
p1.x = 10.0
print(p1) # Point(x=10.0, y=4.0)
@dataclass 只处理带类型注解的类变量。没有类型注解的赋值不被视为数据字段。注意:先声明无默认值字段、再声明有默认值字段会引发 SyntaxError——因为 Python 无法判断构造函数中哪些参数是必传的。如果有默认值的字段放在前面,没有默认值的字段放在后面,调用时会导致参数位置混乱,这与 Python 函数参数的规则一致。
warning
字段声明顺序:没有默认值的字段必须排在有默认值的字段之前。
# BAD — raises dataclasses.SlotWrapperError / SyntaxError at call site
@dataclass
class Bad:
name: str = "Alice"
age: int # ERROR: non-default argument follows default argument
示例 2:field() 配置 — default_factory 与 frozen=True
from dataclasses import dataclass, field
from typing import List
@dataclass
class Classroom:
teacher: str
students: List[str] = field(default_factory=list)
max_size: int = field(default=30, repr=False) # hidden from __repr__
# default_factory avoids shared mutable state between instances
room_a = Classroom("Ms. Wang")
room_a.students.append("Alice")
room_b = Classroom("Mr. Li")
# room_b.students is []. Without default_factory, it would incorrectly share the same list
print(room_a.students) # ['Alice']
print(room_b.students) # []
print(room_a) # Classroom(teacher='Ms. Wang', students=['Alice']) — max_size hidden
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = False
cfg = Config("localhost", 8080)
print(cfg) # Config(host='localhost', port=8080, debug=False)
# Reassignment raises dataclasses.FrozenInstanceError — instance is now immutable
try:
cfg.port = 9090
except dataclasses.FrozenInstanceError as e:
print(f"FrozenInstanceError: {e}")
为什么需要 default_factory? 如果直接使用 students: List[str] = [],所有实例会共享同一个列表对象——这是 Python 中经典的"可变默认值坑"。default_factory 接受一个无参可调对象,每次实例化时调用它来生成独立的新对象。list、dict、set 以及任何无参工厂函数都可以作为 default_factory 的值。
tip
frozen=True 使实例变为不可变的——所有字段赋值都会触发 FrozenInstanceError。这让你可以用数据类安全地表示配置值、常量集合等不应被修改的数据。
示例 3:数据类继承与 __post_init__ 钩子
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Shape:
name: str
color: str = "white"
_area: Optional[float] = field(default=None, init=False)
def area(self) -> Optional[float]:
return self._area
@dataclass
class Rectangle(Shape):
width: float
height: float
def __post_init__(self):
# Validation and computed field assignment after auto-generated __init__
if self.width <= 0 or self.height <= 0:
raise ValueError("Width and height must be positive")
self._area = self.width * self.height
@dataclass
class Circle(Shape):
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError("Radius must be positive")
self._area = 3.14159 * self.radius ** 2
rect = Rectangle("Photo Frame", "blue", 10.0, 5.0)
print(rect) # Rectangle(name='Photo Frame', color='blue', width=10.0, height=5.0)
print(rect.area()) # 50.0
circle = Circle("Coin", "gold", 3.0)
print(circle) # Circle(name='Coin', color='gold', radius=3.0)
print(circle.area()) # 28.274309999999997
# Inheritance: parent fields come first, then child fields
# Shape(name, color, _area) → Rectangle(name, color, width, height)
# _area has init=False, so constructor is (name, color, width, height)
__post_init__ 在自动生成的 __init__ 的末尾被调用,适合做两件事:字段校验(确保数据合法性)和计算字段(根据其他字段推导值)。带 init=False 的字段不会出现在构造函数中,只能通过 __post_init__ 或实例方法赋值。继承时,父类字段排在构造函数参数的前面,子类字段排在后面。
note
dataclasses 在 Python 3.10+ 支持 kw_only=True,允许将指定字段标记为仅关键字参数,进一步降低继承时参数顺序的问题。
常见错误与解决
warning
错误 1:可变默认值共享(mutable default argument)
from dataclasses import dataclass
@dataclass
class BadClass:
items: list = [] # BAD — all instances share the same list
a = BadClass()
b = BadClass()
a.items.append(1)
print(b.items) # [1] — unexpected! b sees a mutation
原因:与函数参数的可变默认值问题同源——[] 在类定义时被创建一次,所有实例共享。
解决:使用 field(default_factory=list):
from dataclasses import dataclass, field
@dataclass
class GoodClass:
items: list = field(default_factory=list)
a = GoodClass()
b = GoodClass()
a.items.append(1)
print(b.items) # [] — each instance gets its own independent list
warning
错误 2:比较 frozen dataclass 时混用类型
from dataclasses import dataclass
@dataclass(frozen=True)
class A:
x: int
@dataclass(frozen=True)
class B(A):
y: int
a = A(10)
b = B(10, 20)
print(a == b) # False — type() is checked first
原因:dataclass 的 __eq__ 实现会先比较 type(self) == type(other),再逐项比较字段值。即使子类包含父类的所有字段,不同类型的实例也永远不会相等。
解决:这是正确行为——不同类型的实例本不应相等。需要值相同则显式提取所需字段比较,或不要将两个不同类做等同判断。
最佳实践
- 数据容器优先使用
@dataclass— 比手写__init__、__repr__更简洁,比 namedtuple 更灵活,是现代 Python 的标准实践 - 可变默认值一律用
default_factory—list、dict、set等可变类型永远不要直接赋为默认值,始终用field(default_factory=...) - 配置类和无共享状态对象用
frozen=True— 不可变性消除意外修改的 bug,使实例可哈希(能放入set或作dict的键),也更利于类型检查工具推断
练习
- 使用
@dataclass定义一个Book类,包含字段:title(str)、author(str)、isbn(str)、price(float)、tags(List[str] 默认空列表)。创建一个实例并打印其字符串表示。
查看答案
from dataclasses import dataclass, field
from typing import List
@dataclass
class Book:
title: str
author: str
isbn: str
price: float
tags: List[str] = field(default_factory=list)
book = Book("Learning Python", "Mark Lutz", "978-1449355739", 59.99, ["Python", "Programming"])
print(book)
# Book(title='Learning Python', author='Mark Lutz', isbn='978-1449355739', price=59.99, tags=['Python', 'Programming'])
- 定义一个不可变 dataclass
Vector2D(x: float,y: float),实现__add__魔术方法使其支持v1 + v2返回新的 Vector2D 实例,并添加一个magnitude属性方法计算模长。
查看答案
from dataclasses import dataclass
import math
@dataclass(frozen=True)
class Vector2D:
x: float
y: float
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
@property
def magnitude(self) -> float:
return math.sqrt(self.x ** 2 + self.y ** 2)
v1 = Vector2D(3.0, 4.0)
v2 = Vector2D(1.0, 1.0)
v3 = v1 + v2
print(v3) # Vector2D(x=4.0, y=5.0)
print(v3.magnitude) # 6.4031242374328485
知识检查
-
@dataclass装饰器默认不会生成以下哪个方法?- A.
__init__ - B.
__repr__ - C.
__hash__ - D.
__eq__
- A.
-
为 dataclass 字段设置可变类型(如
list)的默认值时,正确做法是?- A.
items: list = [] - B.
items: list = field(default=[]) - C.
items: list = field(default_factory=list) - D.
items: list = field(init=False)
- A.
-
以下关于
frozen=Truedataclass 的描述,正确的是?- A. 字段可以修改但无法添加新字段
- B. 实例赋值任何字段都会触发
FrozenInstanceError - C. 只能通过
__post_init__设置字段值,其他方法不行 - D.
frozen=True不影响__eq__的比较行为
查看答案
- C —
@dataclass默认生成__init__、__repr__、__eq__;__hash__仅在frozen=True或unsafe_hash=True时生成 - C —
default_factory=list每次实例化调用list()创建独立新对象,避免共享可变默认值 - B —
frozen=True使实例完全不可变,任何字段赋值(包括__post_init__之外的任何位置)都会触发FrozenInstanceError
本章小结
@dataclass自动为带类型注解的类生成__init__、__repr__、__eq__等样板方法field(default_factory=...)解决可变默认值共享问题,是每个 dataclass 使用者的必备知识frozen=True创建不可变实例,消除意外修改并自动支持哈希__post_init__钩子在自动__init__后执行,适合字段校验和计算字段赋值- 数据类继承遵循"父类字段优先"规则,子类扩展父类的同时保留 auto-generated 方法
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| dataclass | 数据类 | 使用 @dataclass 装饰器自动样板方法的类 |
| boilerplate | 样板代码 | 大量重复的手写方法(__init__、__repr__ 等) |
default_factory | 默认工厂函数 | 用于生成可变默认值的无参可调对象 |
frozen=True | 冻结/不可变 | 使 dataclass 实例不可修改字段的配置 |
__post_init__ | 后初始化钩子 | 在自动生成的 __init__ 末尾执行的自定义逻辑 |
| named tuple | 命名元组 | 带字段名的不可变 tuple 子类 |
下一步
- 阶段复习:进阶部分 → 回顾整个进阶部分的知识,检验学习成果
源码链接
- 数据类是 Python 标准库模块,无需额外安装:
from dataclasses import dataclass, field
阶段复习:进阶部分 (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 进行数据分析实战
Python 学习技能树
更新日期: 2026-05-22
当前版本: v0.1.0
学习路线
基础部分 (第 1-11 章) 🟢
↓
进阶部分 (第 12-19 章) 🟡
↓
实战项目 (第 20+ 章) 🔴
基础部分 🟢
前置要求:有基础编程概念(变量、循环、函数)
第 1 章:变量与表达式 ✅ 完成
- 难度: 🟢 入门
- 前置: 无
- 预计时间: 15 分钟
- 检查点:
- 理解变量赋值
- 掌握算术运算符
- 会使用 f-string
第 2 章:基础数据类型 ✅ 完成
- 难度: 🟢 入门
- 前置: 变量
- 预计时间: 20 分钟
- 检查点:
- 理解 str, int, float, bool
- 掌握字符串基本方法
- 了解类型转换
第 3 章:流程控制 ✅ 完成
- 难度: 🟢 入门
- 前置: 变量、数据类型
- 预计时间: 15 分钟
- 检查点:
- 会使用 if/elif/else
- 理解 match/case (3.10+)
- 掌握三元表达式
第 4 章:循环结构 ✅ 完成
- 难度: 🟢 入门
- 前置: 流程控制
- 预计时间: 20 分钟
- 检查点:
- 会使用 for/while
- 理解 break/continue
- 掌握 enumerate/zip
第 5 章:函数基础 ✅ 完成
- 难度: 🟡 中级
- 前置: 循环结构
- 预计时间: 20 分钟
- 检查点:
- 会定义函数
- 理解参数传递
- 掌握 *args/**kwargs
第 6 章:列表与字典 ✅ 完成
- 难度: 🟡 中级
- 前置: 函数基础
- 预计时间: 20 分钟
- 检查点:
- 掌握列表/字典推导式
- 理解集合操作
- 会使用元组解包
第 7 章:文件操作 ✅ 完成
- 难度: 🟡 中级
- 前置: 列表与字典
- 预计时间: 20 分钟
- 检查点:
- 会使用 with 打开文件
- 掌握 pathlib 路径操作
- 理解编码问题
第 8 章:异常处理 ✅ 完成
- 难度: 🟡 中级
- 前置: 文件操作
- 预计时间: 15 分钟
- 检查点:
- 会使用 try/except/else/finally
- 理解异常继承树
- 会自定义异常
第 9 章:模块与包 ✅ 完成
- 难度: 🟡 中级
- 前置: 异常处理
- 预计时间: 15 分钟
- 检查点:
- 理解 import 机制
-
会使用
__name__守卫 -
掌握
__all__导出控制
第 10 章:面向对象编程 ✅ 完成
- 难度: 🟡 中级
- 前置: 模块与包
- 预计时间: 25 分钟
- 检查点:
- 会定义类
- 理解继承与 super()
- 掌握魔术方法
第 11 章:字符串进阶 ✅ 完成
- 难度: 🟡 中级
- 前置: 面向对象编程
- 预计时间: 20 分钟
- 检查点:
- 会使用 re 模块
- 掌握 split/join/strip
- 理解 f-string 高级格式化
进阶部分 🟡
前置要求: 完成基础部分
第 12 章:异步编程 ✅ 完成
- 难度: 🔴 高级
- 前置: 函数基础
- 预计时间: 25 分钟
- 检查点:
- 理解 async/await
- 会使用 asyncio.gather
- 掌握超时控制
第 13 章:FastAPI 路由 ✅ 完成
- 难度: 🟡 中级
- 前置: 异步编程
- 预计时间: 20 分钟
- 检查点:
- 会定义路由
- 理解路径/查询参数
- 掌握自动文档生成
第 14 章:FastAPI 服务器 ✅ 完成
- 难度: 🟡 中级
- 前置: FastAPI 路由
- 预计时间: 25 分钟
- 检查点:
- 会管理服务进程
- 理解 PID 文件
- 掌握信号处理
第 15 章:依赖注入 ✅ 完成
- 难度: 🟡 中级
- 前置: 面向对象编程
- 预计时间: 20 分钟
- 检查点:
- 理解 DI 模式
- 会使用 injector
- 掌握 NewType
第 16 章:数据库操作 ✅ 完成
- 难度: 🟡 中级
- 前置: 异步编程
- 预计时间: 25 分钟
- 检查点:
- 会连接 SQLite/MySQL
- 理解参数化查询
- 掌握 CRUD 操作
第 17 章:JSON 处理 ✅ 完成
- 难度: 🟢 入门
- 前置: 基础数据类型
- 预计时间: 15 分钟
- 检查点:
- 会使用 json.dumps/loads
- 理解自定义编码器
- 掌握 ujson 高性能替代
第 18 章:NumPy 数值计算 ✅ 完成
- 难度: 🔴 高级
- 前置: 列表与字典
- 预计时间: 25 分钟
- 检查点:
- 理解 ndarray
- 掌握广播机制
- 会实现梯度下降
复习与巩固
阶段复习:基础部分 ✅ 完成
- 难度: 🟡 中级
- 前置: 完成第 1-11 章
- 预计时间: 30 分钟
- 内容: 综合练习与自测
阶段复习:进阶部分 ✅ 完成
- 难度: 🔴 高级
- 前置: 完成第 12-18 章
- 预计时间: 30 分钟
- 内容: 综合练习与自测
进度追踪
当前总进度: [18/18 章完成]
完成清单
- 第 1 章:变量与表达式
- 第 2 章:基础数据类型
- 第 3 章:流程控制
- 第 4 章:循环结构
- 第 5 章:函数基础
- 第 6 章:列表与字典
- 第 7 章:文件操作
- 第 8 章:异常处理
- 第 9 章:模块与包
- 第 10 章:面向对象编程
- 第 11 章:字符串进阶
- 第 12 章:异步编程
- 第 13 章:FastAPI 路由
- 第 14 章:FastAPI 服务器
- 第 15 章:依赖注入
- 第 16 章:数据库操作
- 第 17 章:JSON 处理
- 第 18 章:NumPy 数值计算
完成所有章节后: 🎉 恭喜!你已经掌握了 Python 的核心概念!
下一步:
- 运行实战项目
- 参与真实项目
- 贡献开源代码
学习建议
每天学习时间
- 初学者: 30-60 分钟/天
- 有经验的开发者: 1-2 小时/天
- 全职学习: 4-6 小时/天
🧠 基于认知科学的学习方法
微软 Python 培训建议:"Struggling with dynamic typing is part of learning."
15 分钟规则
- 先自己写代码,让解释器报错
- 仔细阅读错误信息(Python 的错误信息通常很友好)
- 如果卡住超过 15 分钟 → 查看答案
- 关掉答案,从头自己写一遍
间隔重复
- 学完一章后,第二天复习 前一章的关键概念
- 每学完 5 章,做一次 综合复习(见复习章节)
- 使用知识检查题测试自己的记忆
主动回忆
- 不要只是阅读代码,自己写一遍
- 合上教程,尝试凭记忆写出关键概念
- 使用"费曼技巧":尝试向别人解释这个概念
最佳实践
- 边学边练 - 每章都要动手练习
- 做笔记 - 记录难点和收获
- 提问 - 在 Python 中文社区提问
- 复习 - 学完一章后复习前一章
- 解释器是你的老师 - 学会阅读错误信息
遇到困难时
- 回到前一章巩固基础
- 看代码示例(每个章节都有)
- 在 community 提问
- 休息后再试
- 记住:感到困惑是完全正常的! Python 的动态特性需要适应,但掌握后你会写出更灵活的代码。
参考资源
官方
中文社区
实践
常见问题 FAQ
本页面收集 Python 学习过程中最常见的问题。如果你有其他问题,欢迎提交 Issue。
入门相关
Python 适合用来做什么?
Python 是一门通用编程语言,特别适合以下场景:
- Web 后端开发:FastAPI、Django、Flask 等框架
- 数据科学与 AI:NumPy、Pandas、PyTorch、TensorFlow
- 自动化脚本:文件处理、网络爬虫、系统管理
- CLI 工具:使用 Click 或 Typer 构建命令行工具
Python 难学吗?
Python 以"语法简洁、接近自然语言"著称,是公认最适合初学者的语言之一。
- 优势:无需手动管理内存,类型动态推断,生态系统丰富
- 挑战:动态类型可能导致运行时错误(可通过类型提示缓解)
我应该学 Python 2 还是 Python 3?
永远选择 Python 3。Python 2 已于 2020 年停止维护。本教程基于 Python 3.10+ 编写。
环境与工具
uv 和 pip 有什么区别?
| 特性 | uv | pip |
|---|---|---|
| 速度 | 极快(Rust 编写,10-100x 提升) | 较慢 |
| 虚拟环境 | 内置管理 | 需配合 venv |
| 依赖解析 | 全局缓存,智能解析 | 每次重新解析 |
| 推荐场景 | 现代 Python 项目首选 | 遗留项目兼容 |
如何检查 Python 版本?
python --version
# 或
python3 --version
为什么推荐用 uv 而不是 pip?
uv 是新一代 Python 包管理器,解决了 pip + venv + pip-tools 的碎片化问题。
- 一条命令完成:
uv sync= 创建虚拟环境 + 安装依赖 - 兼容
pyproject.toml标准 - 速度更快,磁盘占用更小
代码与语法
list 和 tuple 有什么区别?
- list (
[]):可变,可添加/删除/修改元素 - tuple (
()):不可变,创建后不能修改,适合存储固定数据
什么是"Pythonic"代码?
"Pythonic"指符合 Python 设计哲学的代码风格:
- 使用列表推导式而非循环
- 使用
with管理资源 - 使用
enumerate替代range(len()) - 遵循 PEP 8 命名规范
如何处理中文编码问题?
Python 3 默认使用 UTF-8。如果读写文件时遇到乱码:
with open("file.txt", "r", encoding="utf-8") as f:
content = f.read()
进阶问题
什么时候用异步编程?
- 适合:大量 I/O 操作(网络请求、数据库查询、文件读写)
- 不适合:CPU 密集型任务(数学计算、图像处理)— 应使用多线程/多进程
*args 和 **kwargs 是什么?
*args:接收任意数量的位置参数,打包为元组**kwargs:接收任意数量的关键字参数,打包为字典
类型提示会影响运行性能吗?
不会。类型提示在运行时被忽略,仅用于静态检查(如 mypy)和 IDE 提示。
下一步
术语表 (Glossary)
Purpose: Bilingual terminology reference (中文 ↔ English) for consistent translation across all documentation chapters.
Usage: Reference this glossary when writing chapters to ensure terminology consistency.
核心概念 (Core Concepts)
| English | 中文 | 说明 |
|---|---|---|
| Variable | 变量 | 存储数据的名称 |
| Data Type | 数据类型 | 数据的分类(如 int, str, list) |
| Expression | 表达式 | 产生值的代码组合 |
| Statement | 语句 | 执行操作的代码行 |
| Function | 函数 | 可复用的代码块 |
| Method | 方法 | 与对象关联的函数 |
| Parameter | 参数 | 函数定义中的输入变量 |
| Argument | 实参 | 调用函数时传入的实际值 |
| Scope | 作用域 | 变量可访问的代码范围 |
| Module | 模块 | 包含 Python 代码的 .py 文件 |
| Package | 包 | 包含 __init__.py 的模块目录 |
| Namespace | 命名空间 | 名称到对象的映射 |
数据类型 (Data Types)
| English | 中文 | 说明 |
|---|---|---|
| Integer (int) | 整数 | 不带小数点的数字 |
| Float (float) | 浮点数 | 带小数点的数字 |
| String (str) | 字符串 | 文本数据类型 |
| Boolean (bool) | 布尔值 | True 或 False |
| List | 列表 | 有序可变集合 |
| Tuple | 元组 | 有序不可变集合 |
| Dictionary (dict) | 字典 | 键值对映射 |
| Set | 集合 | 无序不重复元素容器 |
| NoneType | 空类型 | 表示"无值" |
| Mutable | 可变 | 创建后可修改 |
| Immutable | 不可变 | 创建后不可修改 |
控制流 (Control Flow)
| English | 中文 | 说明 |
|---|---|---|
| Conditional | 条件语句 | if/elif/else 分支 |
| Loop | 循环 | for/while 重复执行 |
| Iteration | 迭代 | 逐个访问集合元素 |
| Break | 中断 | 跳出循环 |
| Continue | 继续 | 跳过当前循环剩余代码 |
| Match/Case | 模式匹配 | Python 3.10+ 的结构化匹配 |
面向对象 (OOP)
| English | 中文 | 说明 |
|---|---|---|
| Class | 类 | 对象的蓝图 |
| Object/Instance | 对象/实例 | 类的具体实例 |
| Attribute | 属性 | 对象的数据成员 |
| Method | 方法 | 对象的函数成员 |
| Inheritance | 继承 | 子类获取父类特性 |
| Polymorphism | 多态 | 同一接口不同实现 |
| Encapsulation | 封装 | 隐藏内部实现细节 |
| Dunder Method | 魔术方法 | __init__, __str__ 等特殊方法 |
函数式编程 (Functional)
| English | 中文 | 说明 |
|---|---|---|
| Lambda | Lambda 函数 | 匿名单行函数 |
| Closure | 闭包 | 捕获外部变量的函数 |
| Decorator | 装饰器 | 修改函数行为的函数 |
| Generator | 生成器 | 使用 yield 的惰性迭代器 |
| Comprehension | 推导式 | 简洁创建集合的语法 |
| Higher-order Function | 高阶函数 | 接收或返回函数的函数 |
错误与异常 (Errors & Exceptions)
| English | 中文 | 说明 |
|---|---|---|
| Exception | 异常 | 运行时错误对象 |
| Try/Except | 异常捕获 | 处理错误的结构 |
| Raise | 抛出 | 主动触发异常 |
| Traceback | 追踪信息 | 错误调用栈信息 |
| Assertion | 断言 | 调试用条件检查 |
异步与并发 (Async & Concurrency)
| English | 中文 | 说明 |
|---|---|---|
| Async/Await | 异步/等待 | 异步编程语法 |
| Coroutine | 协程 | 可暂停/恢复的函数 |
| Event Loop | 事件循环 | 异步任务调度器 |
| Thread | 线程 | 操作系统级并发单元 |
| Process | 进程 | 独立执行的程序 |
| GIL | 全局解释器锁 | CPython 的线程限制机制 |
工具与生态 (Tools & Ecosystem)
| English | 中文 | 说明 |
|---|---|---|
| Virtual Environment | 虚拟环境 | 隔离的 Python 运行环境 |
| Package Manager | 包管理器 | 安装/管理第三方库的工具 |
| Dependency | 依赖 | 项目所需的外部库 |
| Interpreter | 解释器 | 执行 Python 代码的程序 |
| REPL | 交互式环境 | Read-Eval-Print Loop |
| Linter | 代码检查工具 | 静态分析代码质量 |
| Formatter | 格式化工具 | 自动整理代码风格 |
常用短语 (Common Phrases)
| English | 中文 | 使用场景 |
|---|---|---|
| Runtime | 运行时 | 代码执行期间 |
| Compile time | 编译时 | 代码转换为字节码期间 |
| Type hint | 类型提示 | 标注变量/函数类型 |
| Dynamic typing | 动态类型 | 运行时确定类型 |
| Duck typing | 鸭子类型 | "像鸭子就是鸭子"的类型哲学 |
| Standard library | 标准库 | Python 内置模块集合 |
| Third-party library | 第三方库 | 通过 pip/uv 安装的外部库 |
维护指南
更新外部链接:
- 每季度检查一次官方文档链接
- Python 版本更新时同步更新术语
添加新术语:
- 新章节引入新概念时同步添加到对应分类
- 保持中英文对照一致性
最后更新: 2026-05-22
维护者: Hello Python Documentation Team
Python 常用操作速查
本章节提供 Python 开发中最常用操作的代码片段,方便快速查找和复制使用。每个类别包含 3-5 个实用示例。
字符串操作
创建与转换
# 创建字符串
s1 = "hello"
s2 = 'world'
s3 = """多行
字符串"""
# 类型转换
num_str = str(42)
parsed = int("42")
float_val = float("3.14")
格式化
name = "Alice"
age = 30
# f-string (推荐)
f"Name: {name}, Age: {age}"
# 对齐与精度
f"{name:>10}" # 右对齐
f"{3.14159:.2f}" # 保留 2 位小数
f"{42:b}" # 二进制
常用方法
text = " Hello World "
text.strip() # 去除两端空白
text.lower() # 转小写
text.upper() # 转大写
text.split() # 按空白分割
" ".join(["a", "b"]) # 合并
text.replace("H", "h") # 替换
text.find("World") # 查找位置
列表操作
创建与初始化
# 基本创建
nums = [1, 2, 3]
empty = []
zeros = [0] * 5 # [0, 0, 0, 0, 0]
# 推导式
squares = [x**2 for x in range(5)]
evens = [x for x in range(10) if x % 2 == 0]
常用操作
nums = [1, 2, 3]
nums.append(4) # 末尾添加
nums.insert(0, 0) # 指定位置插入
nums.extend([5, 6]) # 批量追加
nums.pop() # 弹出末尾
nums.remove(2) # 按值删除
nums.sort() # 原地排序
sorted_nums = sorted(nums) # 返回新列表
nums.reverse() # 原地反转
切片
nums = [0, 1, 2, 3, 4, 5]
nums[0:3] # [0, 1, 2]
nums[3:] # [3, 4, 5]
nums[:3] # [0, 1, 2]
nums[::2] # [0, 2, 4] (步长 2)
nums[::-1] # [5, 4, 3, 2, 1, 0] (反转)
字典操作
创建与初始化
# 基本创建
user = {"name": "Alice", "age": 30}
# 推导式
squares = {x: x**2 for x in range(5)}
# 默认值
from collections import defaultdict
counts = defaultdict(int)
常用操作
user = {"name": "Alice", "age": 30}
user.get("name") # 安全访问
user.get("phone", "N/A") # 默认值
user.keys() # 所有键
user.values() # 所有值
user.items() # 键值对
user.update({"role": "admin"}) # 更新
user.pop("age") # 删除并返回值
合并字典 (3.9+)
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2 # {"a": 1, "b": 2}
文件操作
读取文件
# 一次性读取
content = open("file.txt").read()
# 逐行读取
with open("file.txt") as f:
for line in f:
print(line.strip())
# 读取所有行
lines = open("file.txt").readlines()
写入文件
# 覆盖写入
with open("output.txt", "w") as f:
f.write("Hello\n")
# 追加写入
with open("log.txt", "a") as f:
f.write("New entry\n")
路径操作 (pathlib)
from pathlib import Path
path = Path("data/file.txt")
path.exists() # 是否存在
path.is_file() # 是否文件
path.parent # 父目录
path.name # 文件名
path.suffix # 扩展名
path.read_text() # 读取文本
path.write_text("...") # 写入文本
错误处理
基本结构
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")
except Exception as e:
print(f"Unexpected: {e}")
else:
print("No errors")
finally:
print("Always runs")
自定义异常
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
raise ValidationError("email", "Invalid format")
函数与装饰器
参数类型
def func(pos1, pos2, /, *, kw1, kw2):
pass
# / 前为位置参数,* 后为关键字参数
常用装饰器
from functools import lru_cache, wraps
@lru_cache(maxsize=128)
def expensive_computation(n):
return n * n
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
迭代工具
enumerate 与 zip
names = ["Alice", "Bob"]
for i, name in enumerate(names):
print(f"{i}: {name}")
ages = [30, 25]
for name, age in zip(names, ages):
print(f"{name} is {age}")
itertools 常用
from itertools import chain, combinations
# 扁平化
list(chain([1, 2], [3, 4])) # [1, 2, 3, 4]
# 组合
list(combinations([1, 2, 3], 2)) # [(1,2), (1,3), (2,3)]
完整示例代码
所有代码片段均可直接复制使用。更多完整示例请参考:
返回: 目录