About Hello Python

[!IMPORTANT] 最好的学习方式是间隔性重复练习。

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

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

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

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

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

Introduction (介绍)

Python 是一种高级、解释型、通用编程语言。由 Guido van Rossum 于 1991 年发布,现由 Python 软件基金会维护。Python 以"优雅、明确、简单"为设计哲学,强调代码的可读性和简洁性,支持多种编程范式:面向对象、函数式编程、过程式编程。

Python 是全球最受欢迎的编程语言之一,在 TIOBE、PYPL 等排行榜中常年位居前列。它广泛应用于 Web 开发、数据科学、机器学习、人工智能、自动化运维、网络爬虫、科学计算等领域。

Python 的设计哲学被概括为 "The Zen of Python"(Python 之禅),核心原则包括:

  • 优美优于丑陋,明了优于晦涩,简洁优于复杂
  • 可读性很重要 (Readability counts)
  • 一种且最好只有一种方法来做一件事

为什么选择 Python?

  • 易学性:语法简洁接近自然语言,非常适合编程新手入门
  • 强大的生态:超过 40 万个第三方库,覆盖所有领域(Web、数据、AI、自动化…)
  • 多范式支持:面向对象、函数式、过程式编程均可
  • 跨平台:Windows、macOS、Linux 上均可运行
  • 开源:完全免费,社区活跃
  • 生产力高:几行代码即可完成其他语言数十行代码的功能

Getting Started

安装 Python

首先,你需要安装 Python。Hello Python 项目使用 Python 3.10+ 开发(推荐 3.13+)。

macOS

使用 Homebrew 安装:

$ brew install python@3.13
$ python3 --version
Python 3.13.x

Linux

使用系统包管理器安装:

# Ubuntu/Debian
$ sudo apt-get install python3 python3-pip python3-venv

# CentOS/RHEL
$ sudo yum install python3 python3-pip

Windows

Python 官方网站 下载并安装。安装时记得勾选 "Add Python to PATH"。

安装 uv 包管理器

Hello Python 使用 uv 作为包管理器,它比 pip + venv 更快更简洁。

# 安装 uv
$ curl -LsSf https://astral.sh/uv/install.sh | sh

# 验证安装
$ uv --version
uv x.x.x

克隆项目

$ git clone https://github.com/savechina/hello-python.git
$ cd hello-python

安装依赖

# 同步项目依赖
$ uv sync

# 验证 Python 版本
$ python --version
Python 3.13.x

运行示例

# 运行任意示例文件
$ python -m hello_python.basic.datatype_sample
$ python -m hello_python.advance.json_sample

# 或通过 CLI 入口
$ uv run hello greet "World"

运行测试

# 运行所有基础教程测试
$ uv run pytest tests/basic/ -s -v

# 运行特定模块测试
$ uv run pytest tests/basic/test_datatype_sample.py -s -v

Lint 和格式化

# 代码检查
$ uv run ruff check .

# 代码格式化
$ uv run ruff format .

构建文档

Hello Python 使用 mdBook 构建文档。你可以本地预览教程:

# 构建文档
$ mdbook build docs

# 本地启动文档服务
$ mdbook serve docs

文档将在 http://localhost:3000 打开。

项目结构

hello-python/
├── hello_python/        # 教程源码
│   ├── basic/           # 基础教程(变量、数据类型、循环、函数…)
│   ├── advance/         # 进阶教程(异步、FastAPI、数据库、NumPy…)
│   ├── cli/             # Click CLI 入口
│   ├── algo/            # 算法示例
│   └── leetcode/        # LeetCode 解题
├── tests/               # 测试代码(镜像 hello_python 结构)
├── docs/                # mdBook 文档
│   └── src/             # 文档源文件
│       ├── basic/       # 基础教程章节
│       └── advance/     # 进阶教程章节
├── pyproject.toml       # 项目配置和依赖
└── Makefile             # 快捷命令

运行 Makefile 快捷命令

$ make install   # uv sync
$ make test      # pytest -s -v
$ make lint      # ruff check .
$ make format    # ruff format .
$ make build     # uv build

下一步

完成环境配置后,你可以进入 介绍 了解更多关于 Python 的信息,或者直接开始 基础入门 的学习之旅。

基础入门 (Basic Overview)

欢迎学习 Python 基础教程!本系列涵盖 Python 编程的核心概念,配合可运行的代码示例和练习题,让你在动手实践中快速入门。

学习路径

我们推荐按以下顺序学习,每章内容建立在前一章的基础之上:

#章节难度预计时间描述
1变量与表达式15 分钟学习变量赋值和算术运算
2基础数据类型15 分钟掌握字符串、数字、列表、字典
3流程控制15 分钟if/elif/else 分支与 match/case
4循环结构15 分钟for/while 循环、enumerate、zip
5函数基础⭐⭐20 分钟def、参数、lambda、作用域
6列表与字典⭐⭐20 分钟推导式、集合、元组解构
7文件操作⭐⭐20 分钟读写文件、with、pathlib
8异常处理⭐⭐15 分钟try/except、自定义异常
9模块与包⭐⭐15 分钟import、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)。

示例 3:字符串格式化

word = "World"
s2 = f"Format string, Hello {word}. 你好,世界。!"
print(s2)

f-string 是 Python 3.6+ 推荐的格式化方式。在字符串前加 f,用 {} 包裹变量或表达式,即可嵌入值。

常见错误与解决

[!WARNING] 错误 1:类型混用导致报错

"结果是" + 5 会抛出 TypeError,因为字符串和整数不能直接拼接。

解决:使用 str(5) 转字符串,或改用 f-string:f"结果是 {5}"

[!WARNING] 错误 2:除法结果类型不符合预期

5 / 2 在 Python 中返回 2.5(浮点数),而非 2(整数)。

解决:如需整数除法,使用 //5 // 2 返回 2

最佳实践

  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:字符串方法

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}

知识检查

  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 框架、数据库等更强大的内容。

进阶入门 (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阶段复习:进阶部分30 分钟综合复习与自测

[!TIP] 全部 7 个进阶章节预计学习时长约 2.5 小时。每章都配有练习题和自测题。

为什么学这些?

  • 异步编程 → 让你的程序在等待 I/O 时不阻塞,大幅提升性能
  • FastAPI → Python 中最受欢迎的现代 Web 框架,几行代码就能构建 REST API
  • 依赖注入 → 解耦代码、提升可测试性、适用于大型项目
  • 数据库 → 与 MySQL 和 SQLite 交互,构建有状态的应用
  • JSON → 数据交换的事实标准,前后端通信的桥梁
  • NumPy → 科学计算和机器学习的基石

下一步

异步编程 开始你的进阶之旅!

异步编程 (Asynchronous Programming)

导语

想象你在开一家咖啡店:如果只有一位服务员,每个客户点单后服务员必须站在柜台等待咖啡做好才能接待下一位,那队伍早就排到街角了。正确的做法是:服务员接单后把订单交给咖啡师,然后立刻去接待下一位客户——咖啡做好后再通知客户取餐。Python 的异步编程正是这个逻辑:让程序在等待 I/O(网络请求、数据库查询、文件读写)时不阻塞,可以去处理其他任务。当你需要并发处理大量网络请求、构建高并发 Web 服务、或编写定时任务调度器时,异步编程能显著提升效率和响应速度。

学习目标

  • 理解 async/await 语法和协程(coroutine)的工作原理
  • 掌握 asyncio.gather()asyncio.create_task()asyncio.wait_for() 等核心并发模式
  • 学会在异步环境中使用 ThreadPoolExecutor 处理 CPU 密集型任务

概念介绍

异步编程的核心思想是单线程并发——一个线程通过协作式多任务(cooperative multitasking)同时执行多个操作。Python 通过 asyncio 模块提供支持。

几个关键概念:

  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))

输出: {"city": "\u5927\u8fde"} — 中文变成了 Unicode 转义序列


**原因**:`json.dumps()` 默认 `ensure_ascii=True`,将所有非 ASCII 字符转义为 `\uXXXX` 格式。

**解决**:显式设置 `ensure_ascii=False`。

```python
json.dumps(data, ensure_ascii=False)  # {"city": "大连"}

[!WARNING] 错误 2:尝试序列化不支持的类型(如 datetime)

import json
from datetime import datetime

data = {"time": datetime.now()}
json.dumps(data)  # 💥 TypeError: Object of type datetime is not JSON serializable

原因json 模块默认不支持 datetime、自定义类等复杂类型的序列化。

解决:自定义编码器或使用 default 参数:

json.dumps(data, default=str)  # ✅ 将 datetime 转为字符串
# 或定义完整的 CustomEncoder 类(见上方示例 2)

最佳实践

  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收敛迭代过程中目标函数值变化小于阈值,算法停止

下一步

源码链接

阶段复习:进阶部分 (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 进行数据分析实战