About Hello Python

Important

最好的学习方法是间隔性重复学习。

一个编程高手是怎样练成的?惟手熟尔,重在刻意练习。 这意味着不断学习、练习、实践、再实践,直到熟练掌握所有技能。

Hello,Python 是如何产生的?这是我在学习 Python 过程中,不断编写样例代码、积累点滴经验,最终形成的一套系统教程。

Python 是一门非常优秀的编程语言,它语法简洁、可读性强、功能强大,广泛应用于 Web 开发、数据分析、机器学习、自动化脚本、人工智能等领域。对于初学者来说,Python 是入门首选语言——它的语法接近自然语言,学习曲线平缓。

Hello,Python 是一个绝佳的学习起点。通过这个项目,你不仅能快速入门 Python 编程,还能通过编写、调试、运行示例代码,迅速掌握 Python 的核心知识点。它涵盖了从基础语法到高级进阶知识的完整学习路径,包括异步编程、Web 框架(FastAPI)、数据库操作、NumPy 数值计算等内容。

本书使用 Python 3.10+ 编写(测试环境为 Python 3.13.3),包管理器使用 uv,文档使用 mdBook 构建。请查看 Getting Started 了解如何安装和配置开发环境。

下一步

  • 简介 → 了解 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、nameall
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

最佳实践

  1. 优先使用 f-string 而非 %.format() — 更易读且性能更好
  2. 变量名要有意义 — 用 total_price 而非 a,用 user_name 而非 n
  3. 除法要注意类型 — 用 / 得到浮点数,用 // 得到整数

练习

  1. 写一个表达式,计算 1 到 100 的自然数之和。
查看答案
total = (1 + 100) * 100 // 2
print(f"1-100 的和: {total}")  # 5050
  1. 用 f-string 输出一段信息:你今年 25 岁了,其中 25 是变量。
查看答案
age = 25
print(f"你今年 {age} 岁了")

知识检查

  1. 以下哪段代码会抛出 TypeError

    • A. a = 1 + 2
    • B. "结果为:" + 5
    • C. b = 10 / 2
    • D. c = f"{5 + 3}"
  2. 5 // 2 的结果是?

    • A. 2.5
    • B. 2
    • C. 3
    • D. 5
  3. 在 Python 中,变量在使用前需要先声明吗?

    • A. 需要,用 var 声明
    • B. 需要,用 let 声明
    • C. 不需要,直接赋值即可
    • D. 不确定
查看答案
  1. B — 字符串和整数不能直接拼接
  2. B — // 是整数除法,结果为 2
  3. C — Python 是动态类型语言,赋值即声明

本章小结

  • 变量不需要声明类型,赋值即创建
  • 算术运算符包括 +-*/%//
  • / 运算符始终返回浮点数
  • f-string 是字符串格式化的推荐方式
  • 变量命名要有意义,避免单字母名称

术语表

英文中文说明
variable变量存储数据的名称
operator运算符执行计算的符号
expression表达式由值和运算符组合的式子
f-stringf-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)

最佳实践

  1. 统一使用双引号 定义字符串(项目已配置 ruff 规则)
  2. 优先 f-string 进行格式化
  3. 善用内置方法 而非自己造轮子(如 title()strip()

练习

  1. 将字符串 "hello world" 中的每个单词首字母大写。
查看答案
text = "hello world"
print(text.title())  # Hello World
  1. 提取文件名 "report_2024_final.pdf" 的扩展名(不含 .)。
查看答案
filename = "report_2024_final.pdf"
extension = filename.rsplit(".", 1)[1]
print(extension)  # pdf

知识检查

  1. "hello".capitalize() 返回?

    • A. HELLO
    • B. Hello
    • C. hello
    • D. HELLO WORLD
  2. 以下哪种字符串格式化方式性能最好?

    • A. "Name: %s" % name
    • B. "Name: {}".format(name)
    • C. f"Name: {name}"
    • D. 性能没有差异
  3. Python 中如何定义多行字符串?

    • A. 使用 //
    • B. 使用三引号 """..."""
    • C. 使用 \n 拼接
    • D. 无法定义
查看答案
  1. B — .capitalize() 将首字母大写
  2. C — f-string 是编译期优化,性能最佳
  3. B — 三引号保留换行

本章小结

  • Python 是强类型但动态类型语言
  • 字符串可以用单引号、双引号或多引号定义
  • f-string 是最推荐的格式化方式
  • 字符串内置方法丰富,善用 .split().join().strip()
  • 注意类型混用会抛出 TypeError

术语表

英文中文说明
string (str)字符串文本数据类型
integer (int)整数不带小数点的数字
float浮点数带小数点的数字
boolean (bool)布尔值TrueFalse
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 最常见的流程控制是条件分支:根据某个条件的真假(TrueFalse),决定执行哪一段代码。

Python 中 falsy(假值)包括:FalseNone0""(空字符串)、[](空列表)、{}(空字典)。其余值均为 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 _ 是通配匹配,相当于 elsedefault

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:

最佳实践

  1. 优先三元运算符 替代简单的 if/else 赋值
  2. match/case 适合多路分支(3 个以上条件),比 elif 链更清晰
  3. 避免深层嵌套 — 超过 3 层嵌套时考虑提前 return 或提取函数

练习

  1. 写一个函数,判断一个数字是正数、负数还是零,返回对应字符串。
查看答案
def check_number(n):
    if n > 0:
        return "正数"
    elif n < 0:
        return "负数"
    else:
        return "零"

print(check_number(-5))  # 负数
  1. 用三元运算符判断一个字符串是否为空(空字符串返回 "空",否则返回 "非空")。
查看答案
text = ""
result = "空" if not text else "非空"
print(result)  # 空

知识检查

  1. 以下代码输出什么?

    x = 0
    if x:
        print("truthy")
    else:
        print("falsy")
    
    • A. truthy
    • B. falsy
    • C. 报错
    • D. 无输出
  2. match/case 从哪个 Python 版本开始提供?

    • A. 3.6
    • B. 3.8
    • C. 3.10
    • D. 3.12
  3. 三元运算符 a if cond else b 中,当 condTrue 时,返回?

    • A. a
    • B. b
    • C. cond
    • D. True
查看答案
  1. B — 0 是 falsy,进入 else 分支
  2. C — Python 3.10 引入 match/case
  3. A — 条件为真时返回 a

本章小结

  • if/elif/else 是 Python 最基础的条件分支
  • Python 没有传统 switch——用 elif 链或 match/case
  • 三元运算符 a if cond else b 适合简单分支赋值
  • match/case 支持类型匹配和模式解构
  • falsy 值包括 FalseNone0""[]{}

术语表

英文中文说明
conditional条件控制程序分支的判断
if statementif 语句最基本条件分支
elifelifelse if 的缩写
elseelse默认分支
ternary operator三元运算符一行条件表达式
match/casematch/case结构模式匹配

下一步

源码链接

循环结构 (Loops)

导语

重复是程序员最强大的美德之一——但不是让你手动复制粘贴代码。Python 提供了 for 循环和 while 循环来处理重复任务,配合 enumerate()zip() 和列表推导式(comprehension),能让循环代码既简洁又优雅。学会循环,你就掌握了自动化的第一步。

学习目标

  • 掌握 for 循环遍历可迭代对象
  • 学会 while 循环及其适用场景
  • 理解 breakcontinueelse 子句
  • 熟练使用 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)

最佳实践

  1. 优先 for 循环 遍历可迭代对象,需要条件控制再用 while
  2. 善用 enumerate() 替代手动维护计数器 i = 0; for ...; i += 1
  3. 用列表推导式 替代简单的 for + append,保持代码简洁
  4. 避免嵌套超过 2 层 的循环——考虑提取函数或使用生成器
  5. 循环 else 子句 适合搜索场景,但不要滥用——可读性优先

练习

  1. 写一个 for 循环,打印 1 到 10 之间所有能被 3 整除的数字。
查看答案
for i in range(1, 11):
    if i % 3 == 0:
        print(i)
# 输出: 3, 6, 9
  1. 使用 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]

知识检查

  1. 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
  2. 以下代码输出什么?

    for i in range(3):
        if i == 2:
            break
    else:
        print("loop complete")
    
    • A. loop complete
    • B. 无输出
    • C. 报错
    • D. loop complete 打印 3 次
  3. zip([1, 2], ['a', 'b', 'c']) 返回几个配对?

    • A. 3 个 — (1, 'a'), (2, 'b'), (None, 'c')
    • B. 2 个 — (1, 'a'), (2, 'b')
    • C. 报错
    • D. 1 个
查看答案
  1. B — range(start, stop, step) 不包含 stop,步长为 2
  2. B — break 中断了循环,else 子句不执行
  3. B — zip() 以最短序列为准

本章小结

  • for 循环遍历可迭代对象,while 基于条件重复执行
  • break 跳出循环,continue 跳过当前迭代
  • 循环 else 子句在循环正常结束时执行(未被 break
  • enumerate() 提供索引-值对,zip() 并行配对多个序列
  • 列表推导式 [expr for x in iterable if cond] 是 Pythonic 写法

术语表

英文中文说明
for loopfor 循环遍历可迭代对象的循环
while loopwhile 循环基于条件判断的循环
breakbreak跳出整个循环
enumerateenumerate同时获取索引和值的内置函数
zipzip并行遍历多个序列的内置函数

下一步

源码链接

函数基础 (Functions)

导语

函数(function)是将代码"打包"的魔法——把一段可复用的逻辑封装起来,需要时调用即可。从 print()len(),你每天用的内置函数都是别人写好的函数。现在,轮到你自己写了。Python 的函数系统相当强大:默认参数、*args/**kwargs、lambda 表达式、以及独特的 LEGB 变量作用域规则,掌握这些,你的代码就能从"脚本"进化为"工程"。

学习目标

  • 使用 def 定义函数并传递参数
  • 理解位置参数(positional)与关键字参数(keyword)
  • 掌握 *args**kwargs 处理任意参数
  • 学会 lambda 匿名函数
  • 理解 LEGB 变量作用域规则

概念介绍

函数是一段可重复调用的代码块。在 Python 中使用 def(define)关键字定义函数。函数可以接收参数(parameter)、也可以返回值。没有 return 语句的函数默认返回 None

Python 的函数系统有几个独特之处:

  1. 参数可以有默认值 — 调用时可以省略,提高了灵活性
  2. *args 接收任意数量的位置参数 — 打包为元组(tuple)
  3. **kwargs 接收任意数量的关键字参数 — 打包为字典(dict)
  4. 变量作用域遵循 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

解决:位置参数必须在关键字参数之前。要么全部位置,要么全部关键字。

最佳实践

  1. 函数职责单一 — 一个函数只做一件事,超过 30 行考虑拆分
  2. 参数不要超过 5 个 — 参数太多时考虑用字典或 dataclass 传参
  3. 避免可变默认参数 — 默认参数用 None,内部再创建空容器
  4. 写 docstring — 每个函数第一行用三引号字符串描述功能、参数、返回值
  5. 优先具名函数 — lambda 适合简单场景,复杂逻辑用 def 更易维护

练习

  1. 写一个函数 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
  1. 用 lambda 对列表 ["banana", "Apple", "cherry"] 按忽略大小写字母排序。
查看答案
words = ["banana", "Apple", "cherry"]
words.sort(key=lambda w: w.lower())
print(words)  # ['Apple', 'banana', 'cherry']

知识检查

  1. 以下代码输出什么?

    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. 报错
  2. *args 在函数内部是什么类型?

    • A. 列表(list)
    • B. 元组(tuple)
    • C. 字典(dict)
    • D. 集合(set)
  3. 以下哪个是合法的 lambda 表达式?

    • A. lambda x: x + 1
    • B. lambda x: return x + 1
    • C. lambda x: {return x + 1}
    • D. lambda (x): return x + 1
查看答案
  1. B — 可变默认参数被多次调用共享
  2. B — *args 打包为元组
  3. A — lambda 只能包含表达式,不能有 return 关键字

本章小结

  • def 定义函数,可带位置参数、默认参数和返回值
  • *args 接收任意位置参数(元组),**kwargs 接收任意关键字参数(字典)
  • lambda 适合简单的一次性函数,语法 lambda 参数: 表达式
  • LEGB 规则(Local → Enclosing → Global → Built-in)决定变量查找顺序
  • 永远不要用可变对象(列表、字典)作为默认参数

术语表

英文中文说明
function函数封装可复用代码的块
parameter参数函数定义时的占位变量
keyword argument关键字参数name=value 传递的参数
lambdalambda匿名一行函数
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,或确保两边元素数量一致。

最佳实践

  1. 简单的列表生成优先推导式[x**2 for x in items]for + append 更 Pythonic
  2. 字典访问优先 .get():避免因键不存在触发 KeyError;需要默认值时用 .get(key, default)
  3. 集合用于去重和成员关系测试"apple" in {"apple", "banana"} 是 O(1) 操作
  4. 元组解包处理多返回值width, height = get_size() 让代码意图清晰

练习

  1. 用列表推导式生成 1 到 20 之间所有能被 3 整除的数的平方。
查看答案
result = [x * x for x in range(1, 21) if x % 3 == 0]
print(result)  # [9, 36, 81, 144, 225, 324]
  1. 有两个字典 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()) 的副本。

知识检查

  1. [x for x in range(5) if x > 3] 输出什么?

    • A. [0, 1, 2, 3, 4]
    • B. [4]
    • C. [3, 4]
    • D. []
  2. 字典 d = {"a": 1}d.get("b", 0) 返回什么?

    • A. KeyError
    • B. None
    • C. 0
    • D. 1
  3. 集合 {1, 2} | {2, 3} 的结果是?

    • A. {1, 2, 2, 3}
    • B. {2}
    • C. {1, 2, 3}
    • D. {1, 3}
查看答案
  1. B — range(5) 生成 0-4,只有 4 大于 3
  2. C — .get(key, default) 在键不存在时返回默认值
  3. 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.pathpathlib.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")

最佳实践

  1. 永远用 with 打开文件——自动关闭、异常安全,无需手动 f.close()
  2. 大文件逐行读取——for line in f:f.read()f.readlines() 更节省内存
  3. 优先 pathlib——Path.read_text() 一行完成读写,/ 运算符拼接路径优雅清晰
  4. 显式指定 encoding——避免平台差异导致的编码问题

练习

  1. 使用 pathlib 找出当前目录下所有 .md 文件,并打印它们的大小(字节)。
查看答案
from pathlib import Path

for p in Path(".").iterdir():
    if p.suffix == ".md":
        print(f"{p.name}: {p.stat().st_size} bytes")
  1. 读取一个日志文件 app.log,只打印包含 "ERROR" 的行。
查看答案
with open("app.log", "r", encoding="utf-8") as f:
    for line in f:
        if "ERROR" in line:
            print(line.strip())

知识检查

  1. open("file.txt", "a") 的作用是?

    • A. 清空文件重新写入
    • B. 在文件末尾追加内容
    • C. 只读模式打开
    • D. 删除文件
  2. 关于 with open(...) 以下说法正确的是?

    • A. 需要手动调用 .close()
    • B. 只读取文件的一部分
    • C. 代码结束后自动关闭文件
    • D. 仅二进制文件需要
  3. Path(".").iterdir() 返回什么?

    • A. 一个字符串列表
    • B. 当前目录下的 Path 对象迭代器
    • C. 布尔值
    • D. 文件内容
查看答案
  1. B — "a" (append) 在文件末尾追加内容,不会清空已有内容
  2. C — with 是上下文管理器,退出时自动调用 .close()
  3. 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 statementwith 语句确保资源正确释放的代码块
pathlibpathlib面向对象的现代路径操作库
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 可以捕获大多数异常(但不包括 SystemExitKeyboardInterrupt 等),而裸露的 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}")

最佳实践

  1. 精确捕获 — 只捕获你知道如何处理的具体异常类型,别用 except Exception: 兜底一切
  2. 善用 finally 清理资源 — 关闭文件、断开数据库连接等清理操作放 finally,确保异常时也能执行
  3. 自定义异常要有意义 — 异常名要表达业务语义(如 InvalidAgeError 而非 MyError
  4. 异常信息要具体 — 在异常消息中包含出错值和上下文,方便调试

练习

  1. 写一个函数 divide(a, b),捕获 ZeroDivisionErrorTypeError,分别返回友好提示。
查看答案
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")) # 错误:参数必须是数字
  1. 定义一个 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

知识检查

  1. 以下代码中 finally 块在什么情况下执行?

    try:
        risky_operation()
    except ValueError:
        handle_error()
    finally:
        cleanup()
    
    • A. 只在没有异常时执行
    • B. 只在捕获到异常时执行
    • C. 无论是否有异常都执行
    • D. 永远不会执行
  2. 自定义异常类应该继承:

    • A. RuntimeError
    • B. Exception
    • C. 任何内置类
    • D. 不需要继承
  3. raise 的作用是:

    • A. 捕获异常
    • B. 抛出异常
    • C. 忽略异常
    • D. 定义异常类
查看答案
  1. C — finally 块无论如何都会执行,无论是否有异常
  2. B — 自定义异常应继承 Exception(不推荐直接继承 BaseException
  3. B — raise 用于主动抛出异常

本章小结

  • try/except 捕获特定异常,finally 保证清理操作一定执行
  • else 子句在无异常时执行,分离"正常路径"和"异常路径"
  • 多个 except 子句可分别处理不同类型的异常
  • 自定义异常通过继承 Exception 并实现 __init__,用 raise 抛出
  • 永远不要使用裸 except:,始终指定具体的异常类型

术语表

英文中文说明
exception异常程序运行时的错误事件
try/excepttry/except捕获并处理异常的结构
finallyfinally无论异常与否都执行的代码块
custom exception自定义异常继承 Exception 的业务专用异常类
raiseraise主动抛出异常的语句

下一步

源码链接

模块与包 (Modules & Packages)

导语

当你的代码从几十行增长到几千行,把所有东西塞在一个文件里就变成了"维护噩梦"。Python 用模块(module)和(package)来组织代码——每个 .py 文件就是一个模块,包含 __init__.py 的目录就是一个包。import 语句让你可以在不同模块之间共享代码,if __name__ == "__main__" 让模块既能被导入又能独立运行。掌握模块系统,你的代码就从"杂乱的脚本"升级为"结构化的项目"。

学习目标

  • 掌握 importfrom...import 两种导入方式
  • 理解 if __name__ == "__main__" 守卫(guard)的作用
  • 学会 __all__ 控制模块公开接口

概念介绍

模块(module)就是单个 .py 文件。模块有自己的命名空间(namespace)——模块内的变量、函数、类不会污染全局空间。通过 import 语句,你可以访问另一个模块的内容。

import 有两种形式:

  • import math — 导入整个模块,使用时需加前缀 math.pi
  • from 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 mathfrom 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()

解决

  1. 重构——将共享代码提取到第三个模块 c.py
  2. 或使用局部导入:把 import 放在函数内部延迟加载

warning

错误 2:from module import * 污染命名空间

通配导入会把模块的所有公共名称导入当前环境,容易产生命名冲突,且难以追踪名称来源。

解决:始终显式导入需要的对象——from module import a, b, c,明确列出所需名称。

最佳实践

  1. 显式导入优于通配导入from module import specific_name 而非 from module import *
  2. 把测试代码放在 if __name__ == "__main__" — 确保模块被导入时不会执行副作用
  3. __all__ 声明公开 API — 让使用者清楚哪些是稳定接口,哪些是内部实现
  4. 模块按功能分组 — 相关函数放在一起,保持模块"职责单一"

练习

  1. 假设有一个模块 utils.py,其中定义了 helper()_internal() 两个函数。写一段代码,只导入 helper() 而不导入 _internal()
查看答案
from utils import helper

# _internal 不会被导入——即使不定义 __all__,
# 以 _ 开头的名称约定为"内部使用",不会被通配导入
helper()
  1. 写一个模块 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!")

知识检查

  1. 当模块被直接运行时,__name__ 的值是?

    • A. 模块文件名
    • B. "__main__"
    • C. "__name__"
    • D. "__init__"
  2. __all__ 控制的是?

    • A. import module 导入什么
    • B. from module import * 导入什么
    • C. 模块能否被导入
    • D. 模块内部的变量作用域
  3. 以下哪种导入方式是推荐的?

    • A. from math import *
    • B. import mathfrom math import pi
    • C. import *
    • D. include math
查看答案
  1. B — 直接运行时 __name__"__main__",被导入时为模块名
  2. B — __all__ 只控制通配导入 from module import * 的行为
  3. 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 所有类默认继承 objectclass 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__(...) 开头初始化父类。

最佳实践

  1. __init__ 只做赋值 — 不要在构造方法中做复杂计算或 I/O 操作
  2. 优先组合而非继承 — 能用"has-a"关系的组合解决的,不用"is-a"关系的继承
  3. 善用 __str____repr__ — 让自定义对象在 print 和调试中可读

练习

  1. 定义一个 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
  1. 定义一个 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

知识检查

  1. self 在 Python 类方法中代表什么?

    • A. 当前类本身
    • B. 当前实例对象
    • C. 父类对象
    • D. 无实际意义
  2. 子类调用父类的方法应该使用什么?

    • A. self.super()
    • B. super()
    • C. parent()
    • D. base()
  3. __str____repr__ 的区别是什么?

    • A. 没有区别,只是命名不同
    • B. __str__ 面向用户,__repr__ 面向开发者
    • C. __str__ 用于调试,__repr__ 用于打印
    • D. __str__ 在 Python 2 中使用,__repr__ 在 Python 3 中使用
查看答案
  1. B — self 指向调用该方法的实例对象
  2. B — super() 返回父类的代理对象
  3. 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 对象或 None
  • re.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() 即可。

最佳实践

  1. 正则表达式用 r"..." 原始字符串 — 避免反斜杠带来的转义陷阱
  2. 字符串拼接优先 join() — 比循环 + 性能更好
  3. 善用 f-string 格式说明符 — 对齐、精度、进制转换一行搞定

练习

  1. 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']
  1. join() 和生成器表达式,将列表 ["apple", "banana", "cherry"]" | " 连接成大写字符串。
查看答案
fruits = ["apple", "banana", "cherry"]
result = " | ".join(f.upper() for f in fruits)
print(result)  # APPLE | BANANA | CHERRY

知识检查

  1. 以下哪个 re 函数返回所有匹配组成的列表?

    • A. re.search()
    • B. re.findall()
    • C. re.sub()
    • D. re.match()
  2. " hello world ".split() 的结果是?

    • A. ['', '', 'hello', '', 'world', '', '']
    • B. ['hello', 'world']
    • C. ['hello', '', 'world']
    • D. 报错
  3. f"{42:b}" 的输出是什么?

    • A. 42
    • B. 0b42
    • C. 101010
    • D. 2a
查看答案
  1. B — re.findall() 返回列表,re.search() 返回单个 match 对象
  2. B — split() 无参数时按任意连续空白分割并自动去除空串
  3. C — :b 将整数转为二进制,42 的二进制是 101010

本章小结

  • re.search() 查第一个匹配,re.findall() 查所有匹配,re.sub() 做替换
  • 正则模式用 r"..." 原始字符串书写,() 定义捕获组
  • split() / join() / strip() 是字符串处理三大高频方法
  • f-string 支持高级格式说明符:对齐(>/</^)、精度(.2f)、进制(:b/:x
  • 字符串不可变(immutable),所有字符串方法都返回新字符串

术语表

英文中文说明
regular expression正则表达式描述文本模式的语法
re modulere 模块Python 内置的正则表达式库
splitsplit按分隔符将字符串分割为列表
joinjoin用分隔符将序列合并为字符串
f-string formattingf-string 格式化使用 {}: 控制输出格式

下一步

源码链接

阶段复习:基础部分 (Review Basic)

恭喜你完成了 Python 基础教程的全部 11 个章节。本节将帮助你巩固关键概念、检验学习成果。

知识清单

以下 11 个项目涵盖了基础部分的核心知识点。逐项自检,确保你对每个主题都有清晰理解:

  • 变量与表达式 — 能用 = 赋值、使用算术运算符(+-*/%//)、使用 f-string 格式化
  • 基础数据类型 — 能区分 str、int、float、bool、list、dict,并知道它们的基本操作
  • 流程控制 — 能使用 if/elif/else 分支、三元运算符、match/case 模式匹配
  • 循环结构 — 能使用 for/while 循环、break/continueenumerate()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}")

自测题库

  1. "Python"[1:4] 的结果是?

    • A. Pyt
    • B. yth
    • C. Pyth
    • D. ytho
  2. 以下哪个语句可以遍历字典的键值对?

    • 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)):
  3. with open("f.txt") as f:with 的作用是?

    • A. 提高读取速度
    • B. 自动关闭文件
    • C. 检查文件是否存在
    • D. 加密文件内容
  4. try...except...else...finally 中,else 块在什么时候执行?

    • A. 无论是否异常都执行
    • B. 只在发生异常时执行
    • C. 只在没有异常时执行
    • D. 在 finally 之后执行
  5. class Dog(Animal): 表示?

    • A. Dog 是 Animal 的父类
    • B. Dog 继承了 Animal
    • C. Dog 和 Animal 没有关系
    • D. Animal 是一个方法
查看答案
  1. B — 切片 [1:4] 取索引 1,2,3 → yth
  2. B — .items() 返回 (key, value) 元组
  3. B — with 是上下文管理器,确保文件自动关闭
  4. C — else 在无异常时执行
  5. B — 括号内是父类,Dog 继承 Animal

回顾章节链接

遇到不熟悉的主题,可回到对应章节复习:

祝学习顺利!进入进阶部分后,你会接触到异步编程、Web 框架、数据库等更强大的内容。

下一步

  • 进阶入门 → 开始进阶部分的学习,探索异步编程、FastAPI、数据库等内容

进阶入门 (Advance Overview)

欢迎进入 Python 进阶教程!在掌握了变量、类型、循环、函数和异常处理等基础知识之后,我们将探索更强大的高级特性,这些特性让你能构建真正的生产级应用。

前提条件:完成 基础部分 的全部 11 个章节。如果你已经熟悉 Python 变量、函数、异常和模块,可以直接从这里开始。

进阶学习路径

#章节难度预计时间描述
1异步编程⭐⭐⭐25 分钟asyncio、async/await、协程、线程池
2FastAPI 路由基础⭐⭐⭐20 分钟FastAPI 入门、路由定义、请求处理
3FastAPI 服务器管理⭐⭐⭐25 分钟服务管理、PID 文件、进程控制
4依赖注入⭐⭐⭐20 分钟injector 库、DI 模式、模块绑定
5数据库操作⭐⭐⭐25 分钟PyMySQL、SQLite、参数化查询
6JSON 数据处理⭐⭐15 分钟json 模块、自定义序列化、日期处理
7NumPy 数值计算⭐⭐⭐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 模块提供支持。

几个关键概念:

  1. 协程(Coroutine) — 用 async def 定义的函数,执行到 await 时会主动让出控制权,等等待的内容完成后恢复执行。协程是异步编程的基本单元。
  2. 事件循环(Event Loop) — 异步程序的"调度中心",负责在协程之间切换:当某个协程在等待 I/O 时,事件循环切换到其他就绪的协程继续执行。
  3. await 关键字 — 告诉事件循环"这件事我需要等待,你去执行别的任务吧"。只有 await 后面的操作才是真正"异步"的。
  4. GIL(全局解释器锁) — Python 的 GIL 使得多线程无法真正实现并行计算,但 asyncio单线程的,不存在 GIL 竞争问题。不过,纯 CPU 密集计算仍会阻塞事件循环,需要配合 ThreadPoolExecutorProcessPoolExecutor 来解决。

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 并行,可以改用 ProcessPoolExecutorloop.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

最佳实践

  1. 永远用 await 而非阻塞调用time.sleep()asyncio.sleep()requests.get()httpx.get()aiohttp,同步数据库驱动 → async 驱动(如 aiomysql

  2. 合理选择并发原语gather() 适合"收集所有结果",create_task() 适合"后台调度",wait_for() 适合"设置超时",as_completed() 适合"谁先完成先处理谁"

练习

  1. 编写一个异步爬虫函数 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",
]))
  1. 实现一个带超时的异步函数 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())

知识检查

  1. 以下哪个函数是 asyncio 中正确的非阻塞延迟调用?

    • A. time.sleep(1)
    • B. asyncio.sleep(1)
    • C. await sleep(1)
    • D. async sleep(1)
  2. asyncio.gather() 的返回值顺序与什么有关?

    • A. 任务完成的先后顺序
    • B. 传入任务的顺序
    • C. 随机顺序
    • D. 按任务耗时排序
  3. 在 Python 异步编程中,GIL 对以下哪种场景影响最大?

    • A. 大量并发网络请求(I/O 密集型)
    • B. 大量并发文件读写(I/O 密集型)
    • C. 大规模数值计算(CPU 密集型)
    • D. 定时器调度(CPU 密集型)
查看答案
  1. B — asyncio.sleep(1) 是协程,必须用 await 调用
  2. B — gather() 按传入顺序返回结果,与完成时间无关
  3. C — GIL 限制多线程/单线程中的 CPU 并行计算,I/O 密集型不受影响

本章小结

  • async/await 是 Python 异步编程的核心语法,async def 定义协程,await 挂起等待
  • asyncio.gather() 并发执行多个协程并按传入顺序返回结果
  • asyncio.create_task() 创建独立调度任务,asyncio.wait_for() 为任务设置超时
  • I/O 密集型场景用 asyncio 效果最好,CPU 密集型任务需配合 ThreadPoolExecutorProcessPoolExecutor
  • 永远不要在 async def 中使用阻塞调用(如 time.sleep()),要用异步替代方案

术语表

英文中文说明
coroutine协程async def 定义的异步函数,可被 await 挂起和恢复
await等待关键字挂起当前协程,等待异步操作完成后恢复
event loop事件循环异步程序的调度中心,负责在协程之间切换执行
asyncio异步 I/O 模块Python 标准库中的异步编程框架,提供事件循环和协程工具
GIL全局解释器锁Python 解释器级别的锁,限制多线程并行执行字节码

下一步

源码链接

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 的优势——自动类型验证阻止了无效请求。

最佳实践

  1. 始终使用 async def 对于 I/O 密集型路由
  2. 利用类型提示 — FastAPI 用它来生成文档和验证请求
  3. 先写好模型 — 用 Pydantic BaseModel 定义请求/响应数据结构

练习

  1. 定义一个路由 GET /greet/{name},返回 {"greeting": "Hello, <name>!"}.
查看答案
@app.get("/greet/{name}")
async def greet(name: str):
    return {"greeting": f"Hello, {name}!"}
  1. 定义一个路由 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"}

知识检查

  1. FastAPI 自动生成的 API 文档可以通过哪个路径访问?

    • A. /api
    • B. /docs
    • C. /swagger
    • D. /admin
  2. @app.post("/users") 路由可以处理哪种 HTTP 请求?

    • A. GET
    • B. POST
    • C. PUT
    • D. DELETE
  3. FastAPI 的路径参数 {user_id} 支持类型注解吗?

    • A. 不支持,所有路径参数都是字符串
    • B. 支持,可以在函数参数上标注类型
    • C. 只有整数类型支持
    • D. 只支持可选参数
查看答案
  1. B — FastAPI 自动生成 /docs (Swagger UI) 和 /redoc
  2. B — @app.post() 处理 POST 请求
  3. B — FastAPI 支持所有 Python 类型注解进行自动验证

本章小结

  • FastAPI 是基于 ASGI 的现代 Python Web 框架
  • @app.get() 等装饰器注册路由处理器
  • 返回值字典自动序列化为 JSON
  • uvicorn 是推荐的生产级 ASGI 服务器
  • FastAPI 自动生成交互式 API 文档(/docs)
  • 路径参数和查询参数都支持类型注解和自动验证

术语表

英文中文说明
route路由HTTP 路径与处理函数的映射
decorator装饰器Python 修饰函数的语法特性
ASGIASGI异步服务器网关接口
endpoint端点API 的具体 URL 路径
serialization序列化Python 对象转换为 JSON 格式

下一步

源码链接

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 在子进程中启动。

最佳实践

  1. 始终使用 PID 文件 管理服务进程生命周期
  2. 注册信号处理 确保优雅关闭(graceful shutdown)
  3. 日志输出到文件 而非 stdout(生产环境中 stdout 不可靠)

练习

  1. 写一个函数,检查 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)}")
  1. 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}")

知识检查

  1. PID 文件的主要作用是?

    • A. 记录服务器日志
    • B. 记录进程 ID 用于后续管理
    • C. 配置服务器端口
    • D. 存储环境变量
  2. signal.SIGTERM 信号的含义是?

    • A. 强制终止进程(无法捕获)
    • B. 请求终止进程(可捕获和处理)
    • C. 暂停进程
    • D. 恢复进程
  3. os.kill(pid, 0) 的作用是?

    • A. 终止指定的进程
    • B. 向进程发送空信号
    • C. 检查进程是否存在,不实际发送信号
    • D. 修改进程优先级
查看答案
  1. B — PID 文件记录进程 ID,用于 stop/restart 操作
  2. B — SIGTERM 是可捕获的终止请求(kill 默认发送)
  3. C — signal 0 是一个特殊的"检查存在"信号

本章小结

  • ServiceManager 封装了服务的完整生命周期
  • PID 文件是管理服务进程的关键工具
  • 信号处理(SIGTERM/SIGINT)确保优雅关闭
  • os.kill(pid, 0) 可以检查进程是否存在
  • uvicorn.run() 是阻塞调用,需要线程/subprocess 管理
  • 日志输出应重定向到文件

术语表

英文中文说明
daemon守护进程在后台持续运行提供服务
PID filePID 文件记录进程 ID 的文件
signal handling信号处理捕获和处理操作系统信号
graceful shutdown优雅关闭在完成当前请求后关闭
ASGI serverASGI 服务器运行 ASGI 应用的服务器

下一步

源码链接

依赖注入 (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 标注的构造函数,发现需要 NameDescription,然后查找对应模块的绑定和 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)。

最佳实践

  1. 使用 NewType 创建类型标识 — 避免同类型不同含义的冲突
  2. 分模块组织绑定 — 相关依赖放在同一个 Module 类中
  3. 优先构造函数注入 — 避免属性注入导致的隐式依赖

练习

  1. 定义一个 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}")
  1. @provider 方法创建一个 DatabaseConnection 实例,依赖 APIUrl
查看答案
class DatabaseModule(Module):
    @provider
    def db_connection(self, url: APIUrl) -> DatabaseConnection:
        return DatabaseConnection(url)

知识检查

  1. @inject 装饰器的作用是:

    • A. 标记函数为异步
    • B. 标注构造函数参数需要依赖注入
    • C. 自动记录函数调用日志
    • D. 创建类的单例实例
  2. @provider 方法返回的值会被 Injector 如何处理?

    • A. 忽略
    • B. 缓存并注入到依赖该类型的对象中
    • C. 仅当第一次调用时缓存,后续重新计算
    • D. 直接丢弃
  3. NewType("Name", str) 的用途是?

    • A. 创建 str 的子类
    • B. 在类型层面区分同名但不同含义的类型
    • C. 提升运行时性能
    • D. 替代 dataclass
查看答案
  1. B — @inject 告诉 Injector 需要注入构造函数参数
  2. B — provider 返回值被缓存并在依赖图中分发
  3. B — NewType 创建一个新类型,仅在类型检查时区分,运行时等价于原类型

本章小结

  • 依赖注入将"创建依赖"与"使用依赖"解耦
  • injector 库提供 Module、@inject@provider 三件套
  • Module 定义绑定,Injector 解析依赖图并注入
  • NewType 是创建类型标识的好方法
  • DI 使单元测试更容易——可以替换依赖为 mock 对象

术语表

英文中文说明
dependency injection依赖注入将依赖从外部注入对象,而非内部创建
module模块一组依赖绑定的集合
binder绑定器负责注册类型映射
provider提供者返回特定类型实例的方法
NewType新类型Python 类型别名机制,用于类型检查

下一步

源码链接

数据库操作 (Database Operations)

导语

在现代 Web 应用中,数据几乎总是持久化存储在数据库中。无论是电商平台的商品库存、社交网络的用户关系,还是博客系统的文章档案,都离不开数据库的支撑。Python 提供了多种数据库交互方式:标准库内置的 sqlite3 适合轻量级场景和开发测试,而 PyMySQL 等第三方库则用于连接 MySQL 等生产级数据库。掌握数据库操作,意味着你的程序不再只是"一次性脚本",而是能够存储、查询和管理真实数据的应用。

学习目标

  • 掌握 SQLite 内存数据库的基本操作(建表、插入、查询、更新、删除)
  • 理解 PyMySQL 连接 MySQL 数据库的完整流程与错误处理
  • 学会使用参数化查询防止 SQL 注入攻击

概念介绍

数据库是结构化存储数据的系统。Python 与数据库交互的核心模式可以用"连接 → 游标 → 执行 → 获取"四个步骤概括:

  1. 连接(Connection) — 建立程序与数据库之间的通信通道。每个数据库驱动(如 sqlite3pymysql)都有自己的连接函数,需要传入连接参数(主机、端口、用户名、密码、数据库名)。
  2. 游标(Cursor) — 连接创建后,通过 cursor() 获取游标对象。游标是执行 SQL 语句的"句柄",所有 SQL 操作都通过游标的 execute() 方法完成。
  3. 执行(Execute) — 游标的 execute() 方法接受 SQL 字符串和参数元组,发送给数据库执行。对于修改数据的操作(INSERT/UPDATE/DELETE),还需要调用连接的 commit() 来提交事务。
  4. 获取(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,) 才是包含一个元素的元组。

解决:单个参数时务必在末尾加逗号。

最佳实践

  1. 始终使用参数化查询 — 所有用户输入、外部数据都通过 ?%s 占位符传入,绝不用字符串拼接构建 SQL
  2. 使用上下文管理器(withwith sqlite3.connect(...) as conn: 自动处理提交/回滚和连接关闭,避免资源泄漏

练习

  1. 使用 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()
  1. 编写一个函数 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" 的用户

知识检查

  1. SQLite 中 fetchone() 的返回值类型是?

    • A. 列表
    • B. 字典
    • C. 元组
    • D. 字符串
  2. 防止 SQL 注入的正确方式是?

    • A. 使用字符串格式化(f-string)
    • B. 对用户输入进行 HTML 转义
    • C. 使用参数化查询(占位符)
    • D. 限制输入长度
  3. 以下哪个 SQL 操作不需要 commit()

    • A. INSERT
    • B. UPDATE
    • C. DELETE
    • D. SELECT
查看答案
  1. C — fetchone() 返回单个元组,代表一行数据
  2. C — 参数化查询将 SQL 和数据分开传输,是防御 SQL 注入的唯一可靠方式
  3. D — SELECT 是只读操作,不会修改数据,不需要 commit

本章小结

  • 数据库操作遵循"连接 → 游标 → 执行 → 获取"的标准模式
  • SQLite 无需服务器,适合学习和原型开发;MySQL 适合生产环境
  • 修改数据后必须 commit() 才能持久化
  • 参数化查询(?%s 占位符)是防止 SQL 注入的唯一可靠方式
  • 使用上下文管理器 with 可以自动管理事务和连接资源

术语表

英文中文说明
database数据库结构化存储和检索数据的系统
connection数据库连接程序与数据库之间的通信通道
cursor游标用于执行 SQL 语句和获取结果的对象
parameterized query参数化查询使用占位符传递参数,而非字符串拼接的查询方式
SQL injectionSQL 注入通过构造恶意输入篡改 SQL 语句的攻击方式

下一步

源码链接

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 模块提供了四组核心函数:

  1. json.dumps()(序列化) — 将 Python 对象转换为 JSON 字符串。dump 中的 s 代表 string。常用参数:indent(缩进空格数,美化输出)、ensure_ascii=False(允许输出中文字符)、cls(自定义编码器类)。
  2. json.loads()(反序列化) — 将 JSON 字符串解析为 Python 对象。loads 中的 s 代表 string
  3. json.dump() — 将 Python 对象直接写入文件对象(File object),而非返回字符串。
  4. json.load() — 从文件对象读取并解析 JSON 数据。

note

JSON 与 Python 数据类型的对应关系:objectdictarrayliststringstrnumberint/floattrue/falseTrue/FalsenullNone。注意 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)

最佳实践

  1. 写文件用 encoding="utf-8" — JSON 文件读写时必须显式指定 UTF-8 编码,避免不同操作系统上的默认编码差异导致乱码
  2. 对不可信输入使用 try-except 包裹 json.loads() — 解析外部来源的 JSON 字符串时,格式可能不符合规范,捕获 json.JSONDecodeError 避免程序崩溃

练习

  1. 编写一个函数 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)
  1. 编写一个自定义编码器,能将 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))

知识检查

  1. json.dumps(data, ensure_ascii=False)ensure_ascii=False 的作用是?

    • A. 加快序列化速度
    • B. 允许非 ASCII 字符(如中文)直接输出
    • C. 跳过字典中的 Unicode 键
    • D. 启用 UTF-16 编码
  2. 以下哪个是 json 模块将文件读取的正确方式?

    • A. json.loads()
    • B. json.load()
    • C. json.read()
    • D. json.parse()
  3. Python 的 None 在 JSON 中等价于什么?

    • A. "null"
    • B. null
    • C. undefined
    • D. empty
查看答案
  1. B — 默认 ensure_ascii=True 会将中文等转为 \uXXXX 转义序列
  2. B — load() 从文件对象读取,loads() 从字符串解析
  3. B — JSON 中的 null(小写,无引号)对应 Python 的 None

本章小结

  • json.dumps()/json.loads() 处理字符串,json.dump()/json.load() 处理文件
  • ensure_ascii=False 使中文正常输出,indent=N 美化排版
  • 自定义 JSONEncoderdefault() 方法可以处理 datetime、自定义类等复杂对象
  • JSON 数据类型与 Python 类型一一映射:object→dict、array→list、null→None
  • 解析外部 JSON 数据时务必处理 JSONDecodeError 异常

术语表

英文中文说明
JSONJavaScript 对象表示法一种轻量级数据交换格式,易于人阅读和编写
serialization序列化将 Python 对象转换为 JSON 字符串(或字节)的过程
deserialization反序列化将 JSON 字符串解析为 Python 对象的过程
JSONEncoderJSON 编码器Python 中负责将对象序列化为 JSON 格式的基类
custom encoder自定义编码器继承 JSONEncoder 重写 default() 方法处理特殊类型的编码器

下一步

源码链接

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 有几个关键差异:

  1. 同质性 — ndarray 中所有元素必须是相同类型(如全部 float64),这避免了 Python 列表中每个元素都要存储类型信息的开销。
  2. 连续性内存 — 数组数据在内存中是连续存储的,CPU 缓存命中率更高,批量操作速度更快。
  3. 向量化 — 对数组的算术运算会自动应用到每个元素,无需写 for 循环。这种"批量处理"思维是科学计算的核心范式。
  4. 广播(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+ 中可用,语法更直观。

最佳实践

  1. 优先使用向量化操作替代循环 — NumPy 的内置函数(+, *, np.sum(), np.mean() 等)在 C 层实现,速度远超 Python for 循环
  2. 理解广播规则,善用隐式扩展 — 不要用 np.tile()np.repeat() 手动复制数组来实现广播,让 NumPy 自动处理,节省内存和计算

练习

  1. 使用 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()}")
  1. 使用梯度下降编写一个线性回归拟合器:给定数据点 x = [1, 2, 3, 4, 5], y = [2, 4, 5, 4, 5],用 y = w*x + b 拟合,迭代更新 wb
查看答案
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}")

知识检查

  1. NumPy 数组中所有元素必须是?

    • A. 相同的值
    • B. 相同的类型
    • C. 相同的维度
    • D. 相同的形状
  2. 以下哪个操作用于 NumPy 矩阵乘法?

    • A. a * b
    • B. a @ b
    • C. a × b
    • D. a.mul(b)
  3. 学习率(learning rate)在梯度下降中的作用是?

    • A. 决定迭代次数
    • B. 控制每次更新的步长
    • C. 决定初始参数的随机种子
    • D. 检测收敛的阈值
查看答案
  1. B — NumPy 数组是同质类型的,这与 Python 列表不同
  2. B — @ 是矩阵乘法运算符,* 是逐元素乘
  3. B — 学习率控制沿负梯度方向每次移动的距离,过大则震荡,过小则收敛慢

本章小结

  • NumPy 的 ndarray 是同类型、连续内存的高效数组结构,性能远超 Python 列表
  • 向量化运算无需 for 循环,a + b 直接对每个元素操作,代码简洁且极速
  • 广播机制允许不同形状的数组进行运算,避免手动重复数据
  • 矩阵乘法用 @ 而非 ** 是逐元素乘)
  • 梯度下降是优化算法的基础,通过迭代更新参数逼近最优解

术语表

英文中文说明
NumPy数值 PythonPython 科学计算的基础库,提供高效的多维数组和数学函数
ndarrayn 维数组NumPy 的核心数据结构,同质类型的连续内存数组
gradient descent梯度下降通过沿负梯度方向迭代更新参数以最小化目标函数的优化算法
learning rate学习率梯度下降中控制每步更新幅度的超参数
convergence收敛迭代过程中目标函数值变化小于阈值,算法停止

下一步

源码链接

装饰器 (Decorators)

导语

装饰器是 Python 中一种极具表现力的设计模式,它允许你在不修改原始函数代码的前提下,为其增加额外的功能。想象你编写了一系列核心业务函数,现在需要为每个函数添加性能监控、日志记录、权限校验或缓存功能——如果不使用装饰器,你需要逐一修改每个函数的内部逻辑,这不仅繁琐而且容易出错。装饰器通过 @ 语法糖,将这些"横切关注点"(cross-cutting concerns)以声明式的方式"包裹"在函数外部,让你的代码既保持纯净又功能丰富。在 FastAPI 路由定义、Flask 视图保护、Django 中间件、Celery 任务调度等主流框架中,装饰器都是不可或缺的核心机制。

学习目标

  • 理解函数作为一等对象(first-class object)的含义及其在装饰器中的应用
  • 掌握装饰器的语法糖 @ 及其等价的手动包裹写法
  • 学会使用 functools.wraps 保留被装饰函数的元数据,以及编写带参数的装饰器

概念介绍

理解装饰器需要先掌握一个核心前提:Python 中函数是一等对象。这意味着函数可以像普通变量一样被赋值、传递、作为参数传入、作为返回值返回,甚至存储在数据结构中。

  1. 函数作为一等对象 — 函数没有"特权",它和整数、字符串一样是对象。你可以把函数赋值给另一个变量(f = my_func),可以把函数作为参数传给另一个函数(call_it(my_func)),也可以从函数中返回另一个函数。这种灵活性是装饰器能工作的基础。

  2. @ 语法糖@decorator 本质上是一种"语法糖":

    @decorator
    def func():
        pass
    # 等价于:
    def func():
        pass
    func = decorator(func)
    

    @ 语法在函数定义后立即用装饰器对象重新绑定函数名,使得增强逻辑的意图一目了然。

  3. 包装函数(Wrapper) — 装饰器本质上是一个接收函数作为参数并返回函数的"高阶函数"。内部通常定义一个 wrapper 函数,在其中执行额外逻辑(如计时、日志),然后通过 *args, **kwargs 将参数透传给原始函数。

  4. functools.wraps — 如果装饰器直接返回一个新的 wrapper 函数,原始函数的 __name____doc____signature__ 等元数据将丢失。@functools.wraps(original_func) 装饰器会将原始函数的元数据复制到 wrapper 上,确保调试、文档生成、内省等操作正常工作。

  5. 带参数的装饰器 — 当需要向装饰器传递配置参数(如 @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 的外层)。对于幂等性不强的装饰器组合,务必仔细验证执行顺序。

最佳实践

  1. 始终使用 @functools.wraps — 在每个装饰器的 wrapper 函数上应用 @functools.wraps(func),保留原始函数的元数据,确保调试和文档工具正常工作
  2. 保持装饰器职责单一 — 每个装饰器只负责一个增强功能(计时、缓存、日志、鉴权等),通过组合多个装饰器实现复杂逻辑,而非在单个装饰器中堆砌功能
  3. 注意闭包变量捕获 — 当在循环中创建装饰器或闭包时,使用默认参数(如 def wrapper(_i=i))避免"延迟绑定"(late binding)问题,否则所有 wrapper 可能捕获到循环变量的最终值

练习

  1. 编写一个 @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
  1. 编写一个 @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

知识检查

  1. 装饰器 @decorator 放在函数定义上方时,等价于以下哪种写法?

    • A. decorator(func)
    • B. func = decorator(func)
    • C. func(decorator)
    • D. decorator = func
  2. functools.wraps 的主要作用是?

    • A. 加速函数执行
    • B. 自动注册函数到某个注册表
    • C. 保留被装饰函数的元数据(__name____doc__ 等)
    • D. 为函数添加参数类型检查
  3. 当使用 @A @B def f(): pass 时,函数的执行顺序是?

    • A. A 的 wrapper 先执行,B 的 wrapper 再执行,最后执行原始 f
    • B. B 的 wrapper 先执行,A 的 wrapper 再执行,最后执行原始 f
    • C. 原始 f 先执行,然后 B,最后 A
    • D. A 和 B 并行执行
查看答案
  1. B — @decorator 等价于函数定义后执行 func = decorator(func)
  2. C — wraps 将原始函数的 __name____doc____module____qualname____annotations____dict____wrapped__ 复制到 wrapper 上
  3. 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.wrapswraps 装饰器将原始函数元数据复制到 wrapper 上的实用装饰器
closure闭包内层函数捕获外层函数变量的现象,是装饰器能记住被装饰函数的关键机制
*args / **kwargs可变参数用于将任意位置参数和关键字参数转发给原始函数的语法
decorator factory装饰工厂返回装饰器函数的函数,用于创建带参数的装饰器
stacked decorators叠加装饰器多个装饰器同时修饰一个函数,从下往上执行包裹

下一步

源码链接

生成器与迭代 (Generators & Iteration)

导语

假设你需要处理一个包含 10 亿行日志的文件。如果用列表把所有行读入内存,你的程序会瞬间耗尽几 GB 内存然后崩溃。但如果逐行读取、处理完就丢弃,内存占用几乎为零——这就是懒加载(lazy evaluation)的力量。Python 的生成器正是实现懒加载的核心工具:它每次只生成一个值,用多少算多少,特别适合处理大数据流、无限序列和管道式数据处理。掌握生成器,你的程序既省内存又高效。

学习目标

  • 理解迭代器协议(__iter__ / __next__)和生成器函数的区别与联系
  • 掌握 yield 关键字的工作原理、生成器表达式以及 yield from 的用法
  • 学会使用 .send().throw().close() 实现生成器的双向通信

概念介绍

生成器是 Python 中一种特殊的迭代器,它通过 yield 关键字实现按需生产值。理解生成器需要先理解迭代器协议。

几个关键概念:

  1. 迭代器协议(Iterator Protocol) — 任何实现了 __iter__()__next__() 方法的对象都是迭代器。__iter__() 返回迭代器自身,__next__() 返回下一个值,没有更多值时抛出 StopIterationfor 循环底层就是通过这个协议工作的。
  2. 生成器函数(Generator Function) — 用 yield 而非 return 返回值的函数。每次调用 next() 时,生成器函数从上次暂停的位置继续执行,保留了所有局部变量和调用栈。这种「暂停-恢复」机制使其内存效率极高。
  3. 生成器表达式(Generator Expression) — 类似列表推导式的语法,但用圆括号而非方括号。例如 (x**2 for x in range(1000000)) 不创建完整列表,而是在需要时逐个产生值。
  4. yield from — 将控制权委托给另一个生成器或可迭代对象。它等价于用 for 循环逐一 yield 子 iterable 的所有元素,但代码更简洁,且正确处理了 send()/throw() 等双向通信。
  5. 双向通信(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()

斐波那契数列是无限序列,用列表存储会无限增长。生成器让它成为可能——每次只计算并返回下一个值,内存中始终只保存 ab 两个变量。注意第二次 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  # ✅ 正确

最佳实践

  1. 大数据处理优先用生成器 — 读取大文件、处理流式数据时,用生成器逐条处理而非一次性加载,内存占用稳定在 O(1)
  2. yield from 替代嵌套的 for-yieldyield from sub_generatorfor item in sub_generator: yield item 更简洁,且正确传递 send()/throw() 信号
  3. 生成器函数内不要混用 return value — PEP 380 规定生成器中的 return value 等价于 raise StopIteration(value),但容易引起误解。需要终止时直接用 returnraise StopIteration

练习

  1. 编写一个生成器函数 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]
  1. 编写一个生成器 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

知识检查

  1. 生成器表达式 (x for x in range(5)) 与列表推导式 [x for x in range(5)] 的主要区别是?

    • A. 语法不同,功能完全一样
    • B. 生成器表达式延迟求值,列表推导式立即求值
    • C. 生成器表达式支持索引访问
    • D. 列表推导式内存效率更高
  2. 以下哪个方法可以将值发送到生成器内部?

    • A. gen.push(value)
    • B. gen.send(value)
    • C. gen.write(value)
    • D. gen.input(value)
  3. 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]
查看答案
  1. B — 生成器表达式只在迭代时逐个产生值(懒加载),列表推导式立即创建完整列表
  2. B — .send(value) 是生成器的内置方法,用于向内部发送值并获取下一个产出值
  3. 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 循环捕获后会正常终止

下一步

源码链接

  • 本章无对应源码文件 — 示例代码可直接在文档中运行

上下文管理器 (Context Managers)

导语

想象你去自助餐厅吃饭:拿起餐盘→取菜→用餐→归还餐盘。这个流程中,"归还餐盘"不能忘,否则餐厅会多收押金。Python 编程中也经常遇到类似场景:打开文件后必须关闭、获取锁后必须释放、建立连接后必须断开。上下文管理器(Context Manager)就是 Python 提供的"自动归还餐盘"机制——它确保资源在使用完毕后一定被正确清理,即使中间发生异常也不会遗漏。with 语句是上下文管理器的语法入口,理解它不仅能写出更安全的代码,还能让你自定义优雅的资源管理工具。

学习目标

  • 掌握 with 语句的工作机制和 __enter__/__exit__ 协议
  • 学会使用 @contextmanager 装饰器以生成器方式定义上下文管理器
  • 掌握 contextlib.suppresscontextlib.redirect_stdout 等实用工具

概念介绍

上下文管理器的核心是资源生命周期管理——在进入代码块时获取资源,在离开代码块时释放资源。Python 通过 with 语句和上下文管理器协议来实现这一模式。

  1. with 语句 — 语法糖,底层调用上下文管理器的 __enter__() 获取资源,代码块执行完毕后(无论正常结束还是异常退出)调用 __exit__() 释放资源。
  2. __enter____exit__ 协议 — 上下文管理器类必须实现这两个魔术方法。__enter__() 在进入 with 块时调用,返回值可通过 as 绑定;__exit__(exc_type, exc_val, exc_tb) 在离开时调用,三个参数分别描述异常的类型、值和追踪信息,如果没有异常则全部为 None
  3. contextlib.contextmanager 装饰器 — 用生成器(yield)替代类来定义上下文管理器。yield 之前的代码相当于 __enter__yield 之后的代码相当于 __exit__。适合简单场景,代码更简洁。
  4. contextlib.suppress — 忽略指定异常类型的上下文管理器,等价于 try-except 但更简洁。
  5. 嵌套上下文管理器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

当需要忽略一两个特定异常时,suppresstry-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()  # ✅ 无论如何都会执行

最佳实践

  1. 优先使用 with 语句管理资源 — 文件、网络连接、锁等同步资源都应用 with 语句包裹,即使对象本身支持手动 close/释放,with 也能保证异常路径下的正确清理
  2. 简单场景用 @contextmanager,复杂场景用类 — 如果上下文管理器需要维护多个状态、或需要除 with 之外调用方法(如示例 1 中的 .elapsed),用类实现;如果只是清理一个资源,@contextmanager 代码更简洁
  3. __exit__ 中不要吞掉非预期异常__exit__() 默认返回 False,仅在明确需要忽略特定异常时返回 True,并在日志中记录被忽略的异常信息

练习

  1. 实现一个 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)
  1. 使用 @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

知识检查

  1. __exit__(exc_type, exc_val, exc_tb) 返回 True 的效果是什么?

    • A. 重新抛出异常
    • B. 吞掉异常,阻止其继续传播
    • C. 忽略返回值,不影响异常流
    • D. 触发二次异常
  2. @contextmanager 装饰器要求被装饰函数是什么类型?

    • A. 普通函数(返回 None)
    • B. 异步函数(async def
    • C. 生成器函数(包含 yield
    • D. Lambda 函数
  3. contextlib.suppress(ValueError) 等价于以下哪种写法?

    • A. try: ... except Exception: pass
    • B. try: ... except ValueError: pass
    • C. try: ... finally: pass
    • D. if ValueError: continue
查看答案
  1. B — 返回 True 会让 Python 认为异常已被处理,不再向外传播
  2. C — @contextmanager 将包含 yield 的生成器函数转换为上下文管理器
  3. 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 statementwith 语句Python 中用于自动管理资源的语法结构
__enter__进入方法上下文管理器在进入 with 块时调用的方法
__exit__退出方法上下文管理器在离开 with 块时调用的方法,负责清理资源
@contextmanager上下文装饰器contextlib 模块提供的装饰器,用生成器定义上下文管理器
contextlib.suppress异常抑制器忽略指定异常类型而不传播的上下文管理器

下一步

源码链接

  • 本章暂无对应的独立源码文件

类型提示 (Type Hints)

导语

Python 是一门动态类型语言——变量不需要声明类型,同一个变量可以在运行时改变类型。这种灵活性带来便利的同时,也让大型项目的代码维护和团队协作变得困难。类型提示(Type Hints)是 Python 3.5 引入的可选语法,允许你为变量、函数参数和返回值标注期望的类型。IDE 可以据此提供智能补全,mypy 等静态检查工具可以在运行前发现类型错误,代码阅读者也能一目了然地理解数据流向。Python 的渐进式类型(gradual typing)哲学让类型提示既强大又灵活——你可以从零开始,逐步为项目添加类型标注。

学习目标

  • 掌握函数参数和返回值的类型注解语法
  • 熟练使用 typing 模块中的 ListDictOptionalUnionTypeAlias 等类型工具
  • 理解 Python 3.10+ 的 X | Y 联合类型语法、TypedDictProtocol 的高级用法

概念介绍

类型提示是 Python 中的可选类型标注系统,核心设计理念是渐进式类型(gradual typing)——你可以在现有代码中逐步添加类型注解,而不影响运行行为。Python 解释器在运行时完全忽略类型提示,它们仅服务于静态分析工具(mypy、pyright)和 IDE(自动补全、重构)。

  1. 基本类型注解 — 函数参数使用 name: type 语法,返回值使用 -> type 语法。例如 def greet(name: str) -> str:。变量注解使用 var: int = 0
  2. typing 模块 — Python 标准库提供 ListDictSetTuple 等泛型容器类型。Optional[X] 等价于 X | NoneUnion[X, Y] 表示"X 或 Y"。
  3. X | Y 联合语法 — Python 3.10 引入的管道符联合类型,比 Union[X, Y] 更简洁。str | NoneOptional[str] 更直观。
  4. TypeAlias — 为复杂类型创建可读的别名,提高代码可维护性。
  5. TypedDict — 为字典定义结构化键值类型,比纯 Dict[str, Any] 更精准。
  6. Protocol — Python 的"结构子类型"(structural subtyping)——定义接口契约,只要对象实现了所需的方法和属性,就视为兼容,不需要显式继承。
  7. 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 实现的是结构子类型——UserConfig 不需要声明继承 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

最佳实践

  1. 优先注解函数签名 — 函数参数和返回值是类型提示的核心价值所在。让 IDE 和 mypy 在函数调用点捕获错误,远比注解局部变量重要
  2. Python 3.10+ 使用 X | Y 而非 Union[X, Y]| 语法更简洁,是 PEP 604 的推荐写法。Optional[X] 可读性好可以保留,但 X | None 也完全可以
  3. 在 CI 中集成 mypy — 类型提示的价值在于工具链。在 GitHub Actions 中添加 mypy 检查步骤,确保类型注解不会被破坏

练习

  1. 使用 TypedDict 定义一个 Product 类型,包含 name: strprice: floattags: 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
  1. 定义 Protocol 名为 Drawable,包含方法 draw(self) -> str。创建 CircleSquare 两个类实现它,编写函数 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

知识检查

  1. Python 的 type hints 在运行时会被执行吗?

    • A. 会,Python 会自动检查传入参数的类型
    • B. 不会,Python 解释器在运行时完全忽略类型提示
    • C. 仅在函数首次调用时检查一次
    • D. 取决于是否安装了 mypy
  2. Python 3.10+ 中,str | None 等价于 typing 模块中的哪个类型?

    • A. Union[str, None]
    • B. Optional[str]
    • C. 两者都等价
    • D. 没有等价类型
  3. TypedDictProtocol 的核心区别是?

    • A. TypedDict 定义字典的键值类型,Protocol 定义对象的行为接口
    • B. TypedDict 用于类,Protocol 用于字典
    • C. TypedDict 在运行时被检查,Protocol 不被检查
    • D. 没有区别,可以互换使用
查看答案
  1. B — Python 解释器运行时不执行类型检查,类型提示仅服务于静态分析工具和 IDE
  2. C — Optional[str] 等价于 Union[str, None],也等价于 str | None,三者语义相同
  3. A — TypedDict 描述字典内部结构(键和对应的值类型),Protocol 描述对象的行为契约(方法和属性)

本章小结

  • 类型提示是 Python 的可选语法,运行时不执行,由 mypy 等静态工具在运行前检查
  • 函数参数使用 name: type 语法,返回值使用 -> type 语法
  • typing 模块提供 ListDictOptionalUnionTypeAliasTypedDictProtocol 等丰富工具
  • Python 3.10+ 的 X | Y 联合类型语法是官方推荐写法,更简洁直观
  • 类型提示的价值依赖于工具链——IDE 自动补全、mypy 静态检查、CI 集成缺一不可

术语表

英文中文说明
type hint类型提示Python 中为变量和函数标注期望类型的可选语法
gradual typing渐进式类型允许在动态类型语言中逐步添加类型标注的理念
mypy静态类型检查器检查 Python 代码类型一致性的工具
Union联合类型表示一个值可以是多种类型之一
Optional可选类型等价于 `X
TypeAlias类型别名为复杂类型表达式创建可读名称
TypedDict类型化字典定义字典的键和对应值类型的结构化标注
Protocol协议Python 的结构子类型机制,定义行为接口契约
structural subtyping结构子类型基于对象拥有的方法和属性判断类型兼容性,而非继承关系

下一步

源码链接

数据类 (Data Classes)

导语

在 Python 中,我们常常需要创建仅用于存储数据的类——比如表示坐标的点、配置参数、API 响应模型等。一个典型的"数据容器"类往往充斥着重复的样板代码:__init__ 赋值、__repr__ 调试输出、__eq__ 相等比较……这些工作乏味且容易出错。Python 3.7 引入的 dataclasses 模块通过 @dataclass 装饰器自动生成这些样板方法,让你用最少代码声明数据结构。数据类在概念上类似命名元组(namedtuple),但更灵活——支持类型注解、默认值、继承和自定义初始化逻辑,是现代 Python 项目中构建数据载体的首选方式。

学习目标

  • 掌握 @dataclass 装饰器的基本用法,理解自动生成的 __init____repr____eq__ 方法
  • 学会使用 field() 配置默认值、default_factoryfrozen=True 不可变语义
  • 理解 __post_init__ 钩子和数据类继承的用法与限制

概念介绍

dataclasses 模块的核心是 @dataclass 装饰器。它扫描类体中带类型注解的字段,自动生成一系列特殊方法:

  1. __init__ — 根据字段声明生成构造函数,支持默认值
  2. __repr__ — 生成可读的字符串表示,格式为 ClassName(field1=value1, field2=value2)
  3. __eq__ — 按字段值逐个比较,判断两个实例是否相等
  4. __lt____le____gt____ge__ — 比较运算(需设置 order=True
  5. __hash__ — 哈希值(仅当 frozen=Trueunsafe_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 接受一个无参可调对象,每次实例化时调用它来生成独立的新对象。listdictset 以及任何无参工厂函数都可以作为 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),再逐项比较字段值。即使子类包含父类的所有字段,不同类型的实例也永远不会相等。

解决:这是正确行为——不同类型的实例本不应相等。需要值相同则显式提取所需字段比较,或不要将两个不同类做等同判断。

最佳实践

  1. 数据容器优先使用 @dataclass — 比手写 __init____repr__ 更简洁,比 namedtuple 更灵活,是现代 Python 的标准实践
  2. 可变默认值一律用 default_factorylistdictset 等可变类型永远不要直接赋为默认值,始终用 field(default_factory=...)
  3. 配置类和无共享状态对象用 frozen=True — 不可变性消除意外修改的 bug,使实例可哈希(能放入 set 或作 dict 的键),也更利于类型检查工具推断

练习

  1. 使用 @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'])
  1. 定义一个不可变 dataclass Vector2Dx: 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

知识检查

  1. @dataclass 装饰器默认会生成以下哪个方法?

    • A. __init__
    • B. __repr__
    • C. __hash__
    • D. __eq__
  2. 为 dataclass 字段设置可变类型(如 list)的默认值时,正确做法是?

    • A. items: list = []
    • B. items: list = field(default=[])
    • C. items: list = field(default_factory=list)
    • D. items: list = field(init=False)
  3. 以下关于 frozen=True dataclass 的描述,正确的是?

    • A. 字段可以修改但无法添加新字段
    • B. 实例赋值任何字段都会触发 FrozenInstanceError
    • C. 只能通过 __post_init__ 设置字段值,其他方法不行
    • D. frozen=True 不影响 __eq__ 的比较行为
查看答案
  1. C — @dataclass 默认生成 __init____repr____eq____hash__ 仅在 frozen=Trueunsafe_hash=True 时生成
  2. C — default_factory=list 每次实例化调用 list() 创建独立新对象,避免共享可变默认值
  3. 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 应用,实现以下功能:

  1. 连接 SQLite 内存数据库,创建一个 todos 表(id, title, completed)
  2. GET /todos — 获取所有待办事项,以 JSON 格式返回
  3. POST /todos — 创建新的待办事项,接收 JSON 请求体 {title: "..."}
  4. 所有数据库操作使用参数化查询
查看答案
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 编码器

查看答案
  1. B — asyncio 适合 I/O 密集型并发(大量网络请求),不适合 CPU 密集型任务
  2. A — 路径参数是 URL 路径的一部分(/users/{user_id}),查询参数以 ? 开头附加在 URL 末尾
  3. B — 参数化查询主要好处是安全性和类型安全,执行计划优化由数据库自行决定,不是参数化的直接好处
  4. Db = [10, 20] 广播到 a 的每一行:[[1,2]+[10,20], [3,4]+[10,20]] = [[11,22],[13,24]]
  5. Bdefault=strjson.dumps() 的参数,当遇到无法直接序列化的对象时调用 str() 转换

下一步

你已经完成了进阶部分的全部学习!以下是继续深入的建议路径:

回顾与巩固:

进阶方向:

  • 探索 FastAPI 的认证与授权(JWT、OAuth2)
  • 学习 SQLAlchemy ORM 进行更高级的数据库操作
  • 尝试异步数据库驱动(如 asyncpgaiomysql
  • 深入 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 分钟规则

  1. 先自己写代码,让解释器报错
  2. 仔细阅读错误信息(Python 的错误信息通常很友好)
  3. 如果卡住超过 15 分钟 → 查看答案
  4. 关掉答案,从头自己写一遍

间隔重复

  • 学完一章后,第二天复习 前一章的关键概念
  • 每学完 5 章,做一次 综合复习(见复习章节)
  • 使用知识检查题测试自己的记忆

主动回忆

  • 不要只是阅读代码,自己写一遍
  • 合上教程,尝试凭记忆写出关键概念
  • 使用"费曼技巧":尝试向别人解释这个概念

最佳实践

  1. 边学边练 - 每章都要动手练习
  2. 做笔记 - 记录难点和收获
  3. 提问 - 在 Python 中文社区提问
  4. 复习 - 学完一章后复习前一章
  5. 解释器是你的老师 - 学会阅读错误信息

遇到困难时

  1. 回到前一章巩固基础
  2. 看代码示例(每个章节都有)
  3. 在 community 提问
  4. 休息后再试
  5. 记住:感到困惑是完全正常的! 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+ 编写。


环境与工具

uvpip 有什么区别?

特性uvpip
速度极快(Rust 编写,10-100x 提升)较慢
虚拟环境内置管理需配合 venv
依赖解析全局缓存,智能解析每次重新解析
推荐场景现代 Python 项目首选遗留项目兼容

如何检查 Python 版本?

python --version
# 或
python3 --version

为什么推荐用 uv 而不是 pip

uv 是新一代 Python 包管理器,解决了 pip + venv + pip-tools 的碎片化问题。

  • 一条命令完成:uv sync = 创建虚拟环境 + 安装依赖
  • 兼容 pyproject.toml 标准
  • 速度更快,磁盘占用更小

代码与语法

listtuple 有什么区别?

  • 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)布尔值TrueFalse
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中文说明
LambdaLambda 函数匿名单行函数
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)]

完整示例代码

所有代码片段均可直接复制使用。更多完整示例请参考:


返回: 目录