2026年7月28日

Python 完全指南:从零基础到能写出像样的程序

一份从零基础出发的 Python 3.12+ 完整指南,覆盖语法、数据结构、函数、面向对象、工程实践、异步编程与常用工具。

基于 Python 3.12+ 编写,示例在 Python 3.14 上验证通过。 阅读顺序建议从头到尾,但每章相对独立,可以随时回查。

0.1 这份指南怎么用

这份指南假设你完全没有编程经验。它不要求你懂数学、懂英语术语、懂计算机原理。

三个使用建议:

第一,一定要动手敲。 阅读代码和写出代码是两种能力。看懂一段代码大约需要 30 秒,自己从空白文件写出来可能需要 10 分钟——这 10 分钟才是真正在学习。所有示例都建议手敲一遍,不要复制粘贴。

第二,遇到不懂的先跳过。 编程知识是网状的,不是线性的。第 4 章可能会提到"函数",而函数在第 11 章才讲。碰到这种情况,先接受"它就是这么用的",往后读,回头自然就懂了。

第三,每章的练习题必须做完再往下。 答案就在题目下方,但请先自己写、跑一遍、看到错误、改对,再看答案。看着答案点头产生的记忆几乎为零。

阅读约定

代码块里的 >>> 表示在交互式解释器中输入,下一行是输出:

python
>>> 1 + 1
2

没有 >>> 的代码块表示写在 .py 文件里的完整程序:

python
name = "Python"
print(name)

注释用 # 开头,是给人看的,Python 会忽略它:

python
# 这一行不会被执行
x = 10  # 行尾也可以写注释

段落中出现的记号:

  • ⚠️ —— 初学者高频踩的陷阱
  • 💡 提示 —— 让代码更好的写法
  • 🔍 延伸 —— 现在可以跳过,将来回来看

0.2 学习路线图

起步 ──► 语法基础 ──► 数据结构 ──► 函数 ──► 面向对象 ──► 进阶特性 ──► 工程实践
第1-3章    第4-7章      第8-10章    第11-13章   第16-18章    第19-24章    第25-28章
                            │
                            └──► 异常/文件(第14-15章)

如果你想尽快写出有用的东西:读完第 1–15 章就足以写自动化脚本、处理文件、抓取数据。第 16 章之后是"让代码更好"而不是"让代码能跑"。

时间预期(每天 1 小时):

  • 第 1–10 章:约 2 周,打好语法地基
  • 第 11–18 章:约 2 周,学会组织代码
  • 第 19–29 章:约 3 周,从"会写"到"写得好"

第 1 部分 · 起步

1. 认识 Python

1.1 Python 是什么

Python 是一门编程语言——一套让你向计算机描述"做什么"的语法规则。你写下文本文件,Python 解释器读取它并执行。

它的核心设计取向是:代码首先是给人读的,其次才是给机器执行的。同样一件事,用 Python 写通常比其他语言短,也更接近自然语言。比如"打印 1 到 5":

python
for i in range(1, 6):
    print(i)

对比 Java 需要类、主方法、类型声明、分号、花括号,Python 只有两行。

1.2 Python 能做什么

领域典型用途常用工具
自动化脚本批量重命名文件、整理表格、定时任务标准库 os/pathlib/shutil
数据分析清洗数据、统计、画图pandas、numpy、matplotlib
人工智能训练模型、调用大模型 APIPyTorch、scikit-learn
Web 后端网站服务器、API 接口Django、FastAPI、Flask
网络爬虫抓取网页数据requests、httpx、Beautiful Soup
科学计算数值模拟、生物信息scipy、sympy
运维工具服务器管理、部署脚本Ansible、Fabric

不太适合的场景:手机 App 前端、浏览器前端(那是 JavaScript 的地盘)、对性能极端敏感的场景(游戏引擎、操作系统内核)。

1.3 关于版本

Python 有过一次著名的断代:Python 2 和 Python 3 不兼容。Python 2 已于 2020 年停止维护,今天你只需要关心 Python 3。网上如果看到 print "hello"(没有括号)这样的代码,那是 Python 2,直接忽略。

Python 3 每年 10 月发一个新版本(3.12、3.13、3.14……),每个版本维护 5 年。截至 2026 年年中,最新稳定版是 3.14

选版本的原则:装最新的正式版,除非某个你必须用的库还不支持。本指南所有内容在 3.12 及以上都能用,涉及更新版本才有的特性时会特别标注。

1.4 Python 是"解释型"语言意味着什么

C 语言这类编译型语言,要先把源代码整体翻译成机器码,生成可执行文件,再运行。Python 不同:解释器逐行读取你的代码并执行。

这带来两个直接后果:

好处:改完代码立刻能跑,不用等编译;可以打开一个交互式环境随时试一行代码。

代价:纯 Python 的计算密集代码通常比 C/Rust 慢一到两个数量级(具体差多少极度依赖场景——调用 numpy 这类底层是 C 的库时差距会小得多);很多错误要到运行到那一行才会暴露(比如变量名写错了,只要那行没被执行,Python 不会提前告诉你)。

🔍 延伸:严格来说 Python 会先把源码编译成"字节码"(.pyc 文件),再由虚拟机执行。这是个实现细节,日常写代码不需要关心,只需知道项目里自动生成的 __pycache__ 文件夹就是干这个的,可以随时删。

2. 安装与运行环境

2.1 检查是否已经装了

打开终端(macOS 上叫"终端 / Terminal",Windows 上用 PowerShell),输入:

bash
python3 --version

如果显示类似 Python 3.14.x,说明已经装好了。Windows 上命令可能是 python --version

⚠️ :macOS 系统自带的可能是老版本 Python,且不建议用它装第三方包(会污染系统环境)。建议单独安装一份。

2.2 安装方式

方式一:官方安装包(最适合新手)

python.org/downloads 下载对应系统的安装包,双击安装。

⚠️ Windows 用户务必注意:安装界面第一屏底部有个 "Add python.exe to PATH" 的勾选框,一定要勾上。不勾的话终端里输入 python 会提示找不到命令,是新手最常见的第一个坑。

方式二:包管理器(更适合长期使用)

macOS(先装 Homebrew):

bash
brew install python

Windows:

powershell
winget install Python.Python.3.14

Ubuntu/Debian:

bash
sudo apt update && sudo apt install python3 python3-pip python3-venv

方式三:uv(现代工具链,推荐给有一定基础的人)

uv 是近两年出现的 Python 工具管理器,用 Rust 写的,速度极快,能同时管理 Python 版本、虚拟环境和依赖包:

bash
# 安装 uv(macOS/Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 用 uv 安装 Python
uv python install 3.14

如果你刚开始学,先用方式一或二就好,第 25 章会详细讲 uv。

2.3 选一个编辑器

写 Python 只需要文本编辑器,但好的编辑器能提供语法高亮、自动补全、错误提示,能省下大量时间。

编辑器适合谁说明
VS Code绝大多数人免费、轻量、生态最好。装官方 Python 扩展即可
PyCharm做大型项目功能最全,社区版免费
Zed / Cursor想要现代体验快,AI 集成好
Jupyter Notebook数据分析分块执行、图表内嵌,不适合写完整程序

VS Code 上手三步

  1. VS Code
  2. 扩展市场搜 "Python",装微软官方那个(含 Pylance 智能提示)
  3. 打开一个 .py 文件,右上角的 ▶️ 按钮即可运行

2.4 三种运行代码的方式

方式一:交互式解释器(REPL)

终端输入 python3 回车,会看到:

Python 3.14.x (main, ...) [...]
Type "help", "copyright", "credits" or "license" for more information.
>>>

(括号里的构建信息取决于你从哪装的 Python,每台机器都不一样,不用在意。)

>>> 是提示符,输入一行按回车立刻执行:

python
>>> 2 + 3
5
>>> "hello".upper()
'HELLO'

退出输入 exit() 或按 Ctrl-D(Windows 是 Ctrl-Z 再回车)。

REPL 最适合验证一个想法:"这个函数返回什么来着?"直接试一下,比查文档快。养成随手开 REPL 的习惯。

💡 提示:安装 IPythonpip install ipython)能得到一个大幅增强的 REPL:自动补全、语法高亮、? 查看帮助。

方式二:运行脚本文件

新建一个文件 hello.py,写入内容,然后:

bash
python3 hello.py

这是最常见的方式。

方式三:在编辑器里点运行

VS Code 里按 F5 或点右上角三角,等价于方式二,只是省了敲命令。

2.5 本章练习

练习 2.1 在终端里启动 Python 交互式解释器,计算 2 ** 100(2 的 100 次方),看看结果。然后退出。

练习 2.2 创建一个文件 first.py,内容是 print("我装好 Python 了"),用终端运行它。

<details> <summary>参考答案</summary>

2.1

$ python3
>>> 2 ** 100
1267650600228229401496703205376
>>> exit()

注意这里的重点:Python 的整数没有大小上限,不会像很多语言那样溢出。

2.2

bash
$ cat first.py
print("我装好 Python 了")
$ python3 first.py
我装好 Python 了

如果提示 python3: command not found,说明 PATH 没配好,回到 2.2 节重装并勾选 "Add to PATH"。 </details>


3. 第一个程序

3.1 Hello, World

python
print("Hello, World!")

拆开看:

  • print 是一个函数(function)——一段打包好的、可以反复调用的功能
  • (...) 表示"调用"这个函数
  • "Hello, World!" 是一个字符串(string),用引号包起来的文本,是传给 print参数(argument)

运行结果:

Hello, World!

3.2 print 的更多用法

python
print("a", "b", "c")          # a b c   —— 多个参数用空格分隔
print("a", "b", sep="-")      # a-b     —— 自定义分隔符
print("no newline", end="")   # 不换行
print()                       # 打印一个空行

3.3 从用户拿输入

python
name = input("请输入你的名字:")
print("你好," + name)

运行:

请输入你的名字:小明
你好,小明

input() 会暂停程序、等待用户敲回车,然后把用户输入的内容作为字符串返回。

⚠️ input() 的返回值永远是字符串,哪怕用户输入的是数字。

python
age = input("年龄:")   # 用户输入 18
print(age + 1)         # 报错!TypeError: can only concatenate str (not "int") to str

要当数字用必须先转换:

python
age = int(input("年龄:"))
print(age + 1)   # 19

3.4 注释

python
# 单行注释

x = 1  # 行尾注释

# 多行注释就是写多个 #
# 像这样
# 一行一个

Python 没有 /* */ 这样的块注释语法。三引号字符串常被当作块注释用,但它实际上是一个字符串对象:

python
"""
这几行在语法上是一个字符串,
没被赋值给任何变量,所以效果上等于注释。
放在函数/类/模块开头时,它有特殊含义(文档字符串)。
"""

什么时候该写注释:解释为什么这么写,而不是做了什么

python
# ❌ 无用注释:代码本身已经说清楚了
count = count + 1  # count 加一

# ✅ 有用注释:解释了代码看不出来的意图
count += 1  # 服务端从 1 开始计数,客户端从 0 开始,这里补偿差值

3.5 缩进:Python 最独特的语法

绝大多数语言用花括号 {} 表示代码块,Python 用缩进

python
if 5 > 3:
    print("这行属于 if 内部")
    print("这行也是")
print("这行在 if 外面,无论如何都会执行")

规则:

  • 冒号 : 后面的代码块必须缩进
  • 同一层级的缩进必须一致
  • 约定用 4 个空格,不要用 Tab(编辑器一般会自动把 Tab 转成空格)

⚠️ :混用 Tab 和空格会报 TabErrorIndentationError,而且肉眼看不出区别。VS Code 里可以开启"显示空白字符"来排查。

这个设计的好处是:代码的视觉结构和逻辑结构必然一致,不会出现"缩进骗人"的情况。

3.6 一个稍微完整的程序

把学到的东西串起来——一个 BMI 计算器:

python
# bmi.py —— 计算身体质量指数
# 分级采用中国成人标准(WS/T 428):正常 18.5-23.9,超重 24.0-27.9,肥胖 ≥28
# 注意与 WHO 国际标准不同(WHO:超重 ≥25,肥胖 ≥30)

print("=== BMI 计算器 ===")

height = float(input("身高(米,如 1.75):"))
weight = float(input("体重(公斤):"))

bmi = weight / (height ** 2)

print(f"你的 BMI 是 {bmi:.1f}")

if bmi < 18.5:
    print("偏瘦")
elif bmi < 24:
    print("正常")
elif bmi < 28:
    print("偏胖")
else:
    print("肥胖")

运行:

=== BMI 计算器 ===
身高(米,如 1.75):1.75
体重(公斤):70
你的 BMI 是 22.9
正常

这里出现了几个还没讲的东西——float()f"..."**if/elif/else——接下来几章会逐个展开。现在只需要感受一下:一个有用的程序大概长什么样。

3.7 本章练习

练习 3.1 写一个程序,问用户的名字和年龄,然后输出 "你好 XXX,明年你就 YY 岁了"

练习 3.2 写一个摄氏度转华氏度的程序。公式:华氏 = 摄氏 × 9 / 5 + 32

练习 3.3 下面代码有什么问题?改对它。

python
print("请输入两个数")
a = input()
b = input()
print("和是", a + b)

<details> <summary>参考答案</summary>

3.1

python
name = input("你的名字:")
age = int(input("你的年龄:"))
print(f"你好 {name},明年你就 {age + 1} 岁了")

3.2

python
celsius = float(input("摄氏温度:"))
fahrenheit = celsius * 9 / 5 + 32
print(f"{celsius}°C = {fahrenheit}°F")

3.3 问题在于 input() 返回字符串,"3" + "5" 得到 "35"(字符串拼接)而不是 8。修正:

python
print("请输入两个数")
a = float(input())
b = float(input())
print("和是", a + b)

</details>


第 2 部分 · 语言基础

4. 变量与基本数据类型

4.1 变量:给数据起名字

python
x = 10
message = "hello"
pi = 3.14159

=赋值,不是数学上的"相等"。它的意思是"把右边的值绑定到左边这个名字上"。

Python 的变量不需要声明类型,也不需要提前定义。同一个变量还可以随时换成别的类型:

python
x = 10        # 现在 x 是整数
x = "hello"   # 现在 x 是字符串,完全合法
🔍 延伸:更准确的心智模型是——变量是贴在对象上的标签,而不是装东西的盒子a = b 不是把 b 的内容复制到 a,而是让 a 和 b 指向同一个对象。这个区别在第 8 章讲可变对象时会变得非常重要。

4.2 命名规则

硬性规则(违反会报错):

  • 只能包含字母、数字、下划线
  • 不能以数字开头
  • 不能是关键字(ifforclass 这些)

约定俗成(不遵守不报错,但是坏代码):

python
user_name = "小明"       # ✅ 变量/函数:小写 + 下划线(snake_case)
MAX_RETRY = 3           # ✅ 常量:全大写
class UserProfile:      # ✅ 类名:首字母大写驼峰(PascalCase)
_internal = 1           # ✅ 前缀下划线:表示"内部使用,别碰"

userName = "小明"        # ❌ 这是 Java/JS 风格,Python 里不这么写
l = 1                   # ❌ 小写 l 和数字 1 难以区分
list = [1, 2]           # ❌ 覆盖了内置函数 list

⚠️ :不要用 listdictstrtypeidsummaxinputfile 这些内置名字当变量名。覆盖之后当前作用域里就用不了原来的功能了,而且报错信息会非常莫名其妙。

查看关键字:

python
>>> import keyword
>>> keyword.kwlist          # 硬关键字:任何位置都不能用作名字
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break',
 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally',
 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal',
 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

>>> keyword.softkwlist      # 软关键字:只在特定语法位置才有特殊含义
['_', 'case', 'match', 'type']

软关键字(soft keyword)是 Python 后来引入的机制:matchcase(3.10 的模式匹配)和 type(3.12 的类型别名)只有出现在对应语法结构里才是关键字,其他地方仍然可以当普通名字用。所以 match = 1 是合法的——不过为了可读性还是别这么写。

4.3 四种基本类型

类型英文名例子说明
整数int42, -7, 0无大小上限
浮点数float3.14, -0.5, 2.0小数
字符串str"hi", 'hi'文本
布尔值boolTrue, False真/假,注意首字母大写

type() 查看类型:

python
>>> type(42)
<class 'int'>
>>> type(3.14)
<class 'float'>
>>> type("hi")
<class 'str'>
>>> type(True)
<class 'bool'>

4.4 整数 int

python
a = 42
b = -17
c = 1_000_000     # 下划线做千位分隔符,纯粹为了可读,值就是 1000000

# 其他进制
binary = 0b1010   # 二进制 → 10
octal = 0o17      # 八进制 → 15
hexa = 0xFF       # 十六进制 → 255

Python 整数任意精度,算多大都不会溢出:

python
>>> 2 ** 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

4.5 浮点数 float

python
x = 3.14
y = -0.001
z = 2.5e3      # 科学计数法 = 2500.0
w = 1e-4       # = 0.0001

⚠️ 坑:浮点数不精确

python
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

这不是 Python 的 bug,是所有使用 IEEE 754 二进制浮点的语言的共同行为——0.1 在二进制里是无限循环小数,只能近似存储。

怎么办

python
# 方法 1:比较时允许误差
import math
math.isclose(0.1 + 0.2, 0.3)   # True

# 方法 2:涉及钱等需要精确的场合,用 Decimal
from decimal import Decimal
Decimal("0.1") + Decimal("0.2")    # Decimal('0.3')  ✅

# 方法 3:分数用 Fraction
from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6)    # Fraction(1, 2)

💡 提示:涉及金额的计算,永远不要用 float。用 Decimal,或者用整数存"分"。

4.6 布尔值 bool

python
is_ready = True
is_done = False

bool 实际上是 int 的子类,True == 1False == 0

python
>>> True + True
2
>>> sum([True, False, True])   # 统计有多少个 True
2

真值判断:Python 里任何对象都能当条件用。以下值被视为"假"(falsy):

python
False, None, 0, 0.0, 0j, "", [], (), {}, set(), range(0)

其他一切都是真,包括 -1"False""0"[0]

python
if []:
    print("不会执行")     # 空列表是假

if [0]:
    print("会执行")       # 非空列表是真,哪怕里面装的是 0

if "False":
    print("会执行")       # 非空字符串永远是真!

⚠️ if "False": 是真。从配置文件或命令行读到的字符串 "false" 别直接当布尔用。

4.7 None:表示"没有值"

python
result = None

None 是一个特殊的单例对象,表示"空"、"还没有值"、"函数没有返回东西"。

python
def f():
    print("hi")

x = f()       # 打印 hi
print(x)      # None —— 函数没有 return,默认返回 None

判断 None 必须用 `is`,不要用 ==

python
if x is None:        # ✅
if x is not None:    # ✅
if x == None:        # ❌ 能work但不地道,且自定义类可能重载 == 导致意外

4.8 类型转换

python
int("42")        # 42       字符串 → 整数
int(3.99)        # 3        截断,不是四舍五入!
int("0b101", 2)  # 5        指定进制
float("3.14")    # 3.14
float(7)         # 7.0
str(42)          # "42"
bool(0)          # False
bool("hi")       # True

⚠️ int(3.99)3 不是 4。要四舍五入用 round()

python
round(3.99)      # 4
round(3.14159, 2)  # 3.14

round() 用的是银行家舍入(四舍六入五取偶),这也常让人意外:

python
>>> round(0.5)
0
>>> round(1.5)
2
>>> round(2.5)
2

转换失败会抛异常:

python
>>> int("abc")
ValueError: invalid literal for int() with base 10: 'abc'

第 14 章会讲怎么优雅处理这种情况。

4.9 动态类型 vs 强类型

Python 是动态类型:变量的类型在运行时确定,可以随时改变。

Python 又是强类型:不会偷偷帮你做不合理的类型转换。

python
>>> "3" + 5
TypeError: can only concatenate str (not "int") to str

对比 JavaScript 的 "3" + 5 === "35"——Python 宁可报错也不猜你的意图。这是好事。

4.10 本章练习

练习 4.1 判断以下表达式的值,然后用 REPL 验证:

python
bool("")
bool(" ")
bool([])
bool([[]])
int(-3.7)
round(-3.5)
True + 1
type(1 / 1)

练习 4.2 下面哪些是合法的变量名?

_x    2fast    my-var    myVar    class    Class    数据    __init__

练习 4.3 写一段代码,让用户输入一个数字,输出这个数字的平方、立方,以及是否大于 100。

练习 4.4 为什么 0.1 + 0.2 != 0.3?写一行代码正确判断它们"实质相等"。

<details> <summary>参考答案</summary>

4.1

python
bool("")      # False —— 空字符串
bool(" ")     # True  —— 有一个空格,非空
bool([])      # False —— 空列表
bool([[]])    # True  —— 含一个元素的列表(那个元素是空列表,但不影响外层)
int(-3.7)     # -3    —— 向 0 截断,不是向下取整
round(-3.5)   # -4    —— 银行家舍入,取偶数
True + 1      # 2     —— bool 是 int 的子类
type(1 / 1)   # <class 'float'> —— / 永远返回 float,即使整除

4.2

  • _x ✅ 合法
  • 2fast ❌ 不能数字开头
  • my-var ❌ 连字符不是合法字符(会被当成减法)
  • myVar ✅ 合法但不符合 Python 风格
  • class ❌ 关键字
  • Class ✅ 合法(关键字区分大小写),但看着像类名,别用作变量
  • 数据 ✅ 合法(Python 3 支持 Unicode 标识符),但一般不推荐
  • __init__ ✅ 合法,但双下划线包围的名字是 Python 保留语义的,别乱用

4.3

python
n = float(input("输入一个数字:"))
print(f"平方:{n ** 2}")
print(f"立方:{n ** 3}")
print(f"大于 100:{n > 100}")

4.4 因为 0.1 和 0.2 在二进制浮点数中无法精确表示,相加后误差累积。

python
import math
math.isclose(0.1 + 0.2, 0.3)   # True

</details>


5. 运算符

5.1 算术运算符

python
7 + 3     # 10   加
7 - 3     # 4    减
7 * 3     # 21   乘
7 / 3     # 2.3333333333333335   除(永远返回 float)
7 // 3    # 2    整除(向下取整)
7 % 3     # 1    取余
7 ** 3    # 343  幂

⚠️ 注意负数的整除和取余

python
>>> -7 // 3
-3          # 向下取整(往负无穷方向),不是截断
>>> -7 % 3
2           # 结果符号跟除数一致

规律:a == (a // b) * b + (a % b) 永远成立。

取余的常见用途

python
n % 2 == 0        # 判断偶数
n % 15 == 0       # 判断能同时被 3 和 5 整除
seconds % 60      # 秒转分秒
i % len(items)    # 循环索引,不会越界

5.2 比较运算符

python
a == b     # 相等
a != b     # 不等
a > b      # 大于
a < b      # 小于
a >= b     # 大于等于
a <= b     # 小于等于

Python 支持链式比较,这是很多语言没有的:

python
>>> x = 5
>>> 1 < x < 10        # 等价于 1 < x and x < 10
True
>>> 0 <= score <= 100  # 判断分数合法,非常好用

5.3 逻辑运算符

python
True and False    # False   —— 两个都真才真
True or False     # True    —— 一个真就真
not True          # False   —— 取反

短路求值and 遇到假立刻停,or 遇到真立刻停。

python
# 常见用法:先检查存在性,再访问
if user is not None and user.is_active:
    ...
# 如果 user 是 None,and 短路,不会执行 user.is_active(否则会报错)

⚠️ and/or 返回的不是布尔值,而是其中一个操作数:

python
>>> "a" and "b"
'b'          # 都真,返回最后一个
>>> "" or "default"
'default'    # 前面是假,返回后面
>>> 0 or None
None

这个特性常被用作"默认值":

python
name = user_input or "匿名"   # user_input 为空字符串时用 "匿名"

但要小心 0"" 是合法值的场景,此时应该用显式判断:

python
port = config_port if config_port is not None else 8080
# 而不是 config_port or 8080 —— 因为 0 是有特殊含义的合法值(表示"让系统随便分配一个端口"),
# 用 or 会把用户明确写的 0 悄悄换成 8080

5.4 赋值运算符

python
x = 5
x += 3     # x = x + 3 → 8
x -= 2     # 6
x *= 2     # 12
x /= 4     # 3.0  (注意变成 float 了)
x //= 2    # 1.0
x **= 3    # 1.0
x %= 2     # 1.0

5.5 身份运算符 is

== 比较值是否相等is 比较是否是同一个对象

python
a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b    # True  —— 内容一样
a is b    # False —— 但是两个不同的列表对象
a is c    # True  —— 同一个对象

什么时候用 is:只在和 NoneTrueFalse 比较,以及确实要判断"同一个对象"时用。

python
if x is None:      # ✅
if x is True:      # 一般写 if x: 就够了
if name is "abc":  # ❌ 永远不要这样比较字符串

⚠️ 坑:小整数缓存

python
>>> a = 256; b = 256
>>> a is b
True          # CPython 缓存了 -5 到 256 的整数
>>> a = 257; b = 257
>>> a is b
False         # 超出缓存范围

这是 CPython 的实现细节,不要依赖它。比较数值永远用 ==

5.6 成员运算符 in

python
"a" in "abc"           # True
3 in [1, 2, 3]         # True
"key" in {"key": 1}    # True  —— 字典查的是键
5 not in [1, 2, 3]     # True

in 在不同类型上的性能差别巨大(第 9 章详谈):

  • 列表/元组:从头找到尾,元素越多越慢(O(n))
  • 集合/字典:直接算位置,几乎瞬间(O(1))
python
# 100 万个元素,判断存在性
big_list = list(range(1_000_000))
big_set = set(big_list)

999_999 in big_list    # 慢
999_999 in big_set     # 快几百倍

5.7 位运算符

日常业务代码用得少,但在处理标志位、底层协议时有用。

python
5 & 3     # 1   按位与     0101 & 0011 = 0001
5 | 3     # 7   按位或     0101 | 0011 = 0111
5 ^ 3     # 6   按位异或   0101 ^ 0011 = 0110
~5        # -6  按位取反
5 << 1    # 10  左移一位(相当于乘 2)
5 >> 1    # 2   右移一位(相当于整除 2)

5.8 运算符优先级

从高到低(不用背,记住不确定就加括号):

**                          幂
+x  -x  ~x                  正负号、按位取反
*  /  //  %                 乘除
+  -                        加减
<<  >>                      移位
&                           按位与
^                           按位异或
|                           按位或
比较  ==  !=  <  >  in  is   比较类
not                         逻辑非
and                         逻辑与
or                          逻辑或
python
2 + 3 * 4        # 14,不是 20
(2 + 3) * 4      # 20
2 ** 3 ** 2      # 512,幂是右结合:2 ** (3 ** 2)
-2 ** 2          # -4,因为 ** 优先级高于负号:-(2 ** 2)

💡 提示:写复杂表达式时主动加括号。少写几个字符不值得让读代码的人(包括三个月后的你)停下来想优先级。

5.9 海象运算符 :=

Python 3.8 引入,在表达式内部赋值:

python
# 传统写法
data = input()
while data != "quit":
    print(data)
    data = input()

# 海象写法
while (data := input()) != "quit":
    print(data)

用得好能减少重复,用多了会伤可读性。典型的好场景:

python
# 避免调用两次函数
if (n := len(items)) > 10:
    print(f"太多了:{n} 个")

5.10 本章练习

练习 5.1 不运行,说出每个表达式的值:

python
17 // 5
-17 // 5
17 % 5
-17 % 5
2 ** 3 ** 2
not 0 or 1
[] or "x"
1 == 1.0
1 is 1.0

练习 5.2 写一个程序:输入秒数,输出 X 小时 Y 分 Z 秒

练习 5.3 写一个判断闰年的表达式。规则:能被 4 整除且不能被 100 整除,或者能被 400 整除。

练习 5.4 用一行代码交换两个变量的值。

<details> <summary>参考答案</summary>

5.1

python
17 // 5      # 3
-17 // 5     # -4   (向下取整,不是 -3)
17 % 5       # 2
-17 % 5      # 3    (符号跟除数)
2 ** 3 ** 2  # 512  (右结合)
not 0 or 1   # True (not 0 → True,True or 1 短路返回 True)
[] or "x"    # 'x'
1 == 1.0     # True (值相等)
1 is 1.0     # False(不同类型,不同对象)

5.2

python
total = int(input("秒数:"))
hours = total // 3600
minutes = total % 3600 // 60
seconds = total % 60
print(f"{hours} 小时 {minutes} 分 {seconds} 秒")

更简洁的写法用 divmod

python
minutes, seconds = divmod(total, 60)
hours, minutes = divmod(minutes, 60)

5.3

python
year = int(input("年份:"))
is_leap = (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
print(is_leap)

标准库也有现成的:import calendar; calendar.isleap(year)

5.4

python
a, b = b, a

这是元组解包(第 8 章),Python 里交换变量不需要临时变量。 </details>


6. 字符串

字符串是最常用的数据类型。这一章会讲得比较细,因为处理文本是编程中占比最高的工作之一。

6.1 创建字符串

python
s1 = "双引号"
s2 = '单引号'          # 完全等价
s3 = """三引号
可以跨行"""
s4 = '''也可以用单三引号'''

单双引号等价,选一种保持一致即可(Python 社区略偏好双引号)。有引号冲突时灵活切换:

python
"他说:'你好'"        # ✅ 外双内单
'He said: "hi"'      # ✅ 外单内双
"He said: \"hi\""    # 也可以转义,但没上面好看

6.2 转义字符

python
"\n"     # 换行
"\t"     # 制表符
"\\"     # 反斜杠本身
"\""     # 双引号
"\'"     # 单引号
"\u4e2d" # Unicode 码点 → 中

原始字符串 r"...":关掉转义,正则表达式和 Windows 路径必备。

python
print("C:\new\table")       # \n 和 \t 被当成转义了,输出乱七八糟
print(r"C:\new\table")      # C:\new\table  ✅

6.3 f-string:字符串格式化的正确方式

f-string(Python 3.6+)是日常字符串格式化的首选

python
name = "小明"
age = 18

f"我叫{name},今年{age}岁"           # 我叫小明,今年18岁
f"明年 {age + 1} 岁"                # 可以放任何表达式
f"{name.upper()}"                  # 可以调用方法

格式说明符(冒号后面):

python
pi = 3.14159265

f"{pi:.2f}"        # '3.14'      保留 2 位小数
f"{pi:10.2f}"      # '      3.14' 宽度 10,右对齐
f"{pi:<10.2f}"     # '3.14      ' 左对齐
f"{pi:^10.2f}"     # '   3.14   ' 居中
f"{pi:+.2f}"       # '+3.14'     总是显示符号

n = 1234567
f"{n:,}"           # '1,234,567'  千位分隔
f"{n:_}"           # '1_234_567'
f"{n:>12}"         # '     1234567' 右对齐宽度 12
f"{n:012}"         # '000001234567' 补零

r = 0.8567
f"{r:.1%}"         # '85.7%'      百分比

f"{255:b}"         # '11111111'   二进制
f"{255:o}"         # '377'        八进制
f"{255:x}"         # 'ff'         十六进制
f"{255:X}"         # 'FF'
f"{255:08b}"       # '11111111'   补零到 8 位

调试神器 `=`(Python 3.8+):

python
x = 42
print(f"{x=}")           # x=42
print(f"{x * 2 = }")     # x * 2 = 84

print("x =", x) 省事得多,写临时调试语句时非常好用。

其他格式化方式(了解即可,看老代码时会遇到):

python
"我叫{},{}岁".format(name, age)      # str.format(),Python 3.0+
"我叫%s,%d岁" % (name, age)          # % 格式化,最老,来自 C

日常拼字符串统一用 f-string 就好。另外两种并没有被废弃,在这些场景仍然常见:

  • 日志logging 推荐 logger.info("user %s logged in", name),这样只有日志真正要输出时才做格式化,级别被过滤掉时零开销。见第 27 章。
  • 模板复用:格式串需要提前定义、事后填值时(如国际化文案、配置里的模板),str.format()string.Template 才做得到——f-string 是在定义处立刻求值的。

6.4 字符串是不可变的

python
s = "hello"
s[0] = "H"     # TypeError: 'str' object does not support item assignment

所有"修改"字符串的方法,实际上都返回新字符串

python
s = "hello"
t = s.upper()
print(s)   # hello  —— 原来的没变
print(t)   # HELLO

6.5 索引与切片

python
s = "Python"
#    012345      正向索引
#   -654321      负向索引

s[0]      # 'P'
s[5]      # 'n'
s[-1]     # 'n'   最后一个
s[-2]     # 'o'   倒数第二个
s[10]     # IndexError!

切片 s[start:stop:step],规则:含头不含尾

python
s = "Python"

s[0:3]     # 'Pyt'    索引 0,1,2
s[:3]      # 'Pyt'    省略 start 表示从头
s[3:]      # 'hon'    省略 stop 表示到尾
s[:]       # 'Python' 整个复制
s[1:5:2]   # 'yh'     步长 2
s[::-1]    # 'nohtyP' 步长 -1 = 反转
s[::2]     # 'Pto'    每隔一个取
s[-3:]     # 'hon'    最后三个
s[:-3]     # 'Pyt'    去掉最后三个

⚠️ 切片不会越界报错,这跟索引不同:

python
s[100:200]    # ''  空字符串,不报错
s[0:100]      # 'Python'

切片规则对列表、元组同样适用,是 Python 里最常用的操作之一。

6.6 常用字符串方法

大小写

python
"hello".upper()        # 'HELLO'
"HELLO".lower()        # 'hello'
"hello world".title()  # 'Hello World'
"hello".capitalize()   # 'Hello'
"Hello".swapcase()     # 'hELLO'
"Hello".casefold()     # 'hello'  比 lower() 更彻底,适合不区分大小写比较

去空白

python
"  hi  ".strip()       # 'hi'      两端
"  hi  ".lstrip()      # 'hi  '    左边
"  hi  ".rstrip()      # '  hi'    右边
"xxhixx".strip("x")    # 'hi'      去掉指定字符
"file.txt".removesuffix(".txt")   # 'file'  Python 3.9+
"pre_name".removeprefix("pre_")   # 'name'

⚠️ strip("abc") 不是去掉子串 "abc",而是去掉两端任何 a、b、c 字符。要去掉子串用 removeprefix/removesuffix

查找与判断

python
"hello".find("ll")        # 2    找不到返回 -1
"hello".index("ll")       # 2    找不到抛 ValueError
"hello".rfind("l")        # 3    从右边找
"hello".count("l")        # 2
"hello".startswith("he")  # True
"hello".endswith((".jpg", ".png"))   # 可以传元组,任一匹配即可
"ll" in "hello"           # True  ← 只判断存在,用这个最简单

替换

python
"a-b-c".replace("-", "+")       # 'a+b+c'
"a-b-c".replace("-", "+", 1)    # 'a+b-c'  只替换第一个

分割与拼接

python
"a,b,c".split(",")           # ['a', 'b', 'c']
"a b  c".split()             # ['a', 'b', 'c']  不带参数按任意空白分,且忽略连续空白
"a,b,c".split(",", 1)        # ['a', 'b,c']     最多分 1 次
"a,b,c".rsplit(",", 1)       # ['a,b', 'c']     从右边分
"line1\nline2".splitlines()  # ['line1', 'line2']
"a=1".partition("=")         # ('a', '=', '1')  分成三段

",".join(["a", "b", "c"])    # 'a,b,c'
"".join(["a", "b"])          # 'ab'
"\n".join(lines)             # 用换行连接

⚠️ join 的调用方是分隔符,参数是列表。写成 ["a","b"].join(",") 是错的(这是 JavaScript 的写法)。

💡 提示:拼接大量字符串时用 join,不要用 += 循环累加:

python
# ❌ 慢:每次 += 都创建新字符串
result = ""
for word in words:
    result += word

# ✅ 快
result = "".join(words)

内容判断

python
"abc".isalpha()      # True   全是字母
"123".isdigit()      # True   全是数字
"a1".isalnum()       # True   字母或数字
"  ".isspace()       # True   全是空白
"Hello".istitle()    # True
"ABC".isupper()      # True

⚠️ "".isdigit()False(空字符串),"3.14".isdigit() 也是 False(有小数点)。判断能否转成数字最可靠的方式是 try/except(第 14 章)。

对齐与填充

python
"5".zfill(3)         # '005'
"hi".ljust(10, ".")  # 'hi........'
"hi".rjust(10)       # '        hi'
"hi".center(10, "*") # '****hi****'

6.7 字符串与编码

Python 3 的 str 存的是 Unicode 字符bytes 存的是字节。它们是两种不同的类型。

python
s = "中文"
b = s.encode("utf-8")     # str → bytes: b'\xe4\xb8\xad\xe6\x96\x87'
s2 = b.decode("utf-8")    # bytes → str: '中文'

len(s)    # 2  两个字符
len(b)    # 6  六个字节(UTF-8 里一个汉字占 3 字节)

记忆法:encode = 编码 = 人类文字 → 机器字节;decode = 解码 = 字节 → 文字。

⚠️ :读文件时如果不指定编码,Python 会用系统默认编码,在中文 Windows 上可能是 GBK,读 UTF-8 文件就会乱码或报错。永远显式写 `encoding="utf-8"`

python
open("file.txt", encoding="utf-8")

6.8 字符串比较与排序

python
"apple" < "banana"     # True   按字符的 Unicode 码点逐位比较
"Apple" < "apple"      # True   大写字母码点小于小写
"10" < "9"             # True   ⚠️ 字符串比较不是数值比较!

不区分大小写地比较:

python
a.casefold() == b.casefold()

6.9 本章练习

练习 6.1 给定 s = "Hello, World!",写出得到以下结果的表达式:

  1. "!dlroW ,olleH"(反转)
  2. "HELLO, WORLD!"
  3. "World"
  4. "Hello"
  5. 字母 l 出现的次数

练习 6.2 写一个函数式的处理:给定用户输入的一串逗号分隔的名字(可能有多余空格,如 " 张三, 李四 ,王五 "),输出清洗后的列表。

练习 6.3 写一个程序判断输入的字符串是否是回文(正反读一样,忽略大小写和空格)。

练习 6.4 给定 path = "/home/user/documents/report.pdf",用字符串方法提取出文件名 report.pdf 和扩展名 pdf

练习 6.51234567.891 格式化成 "1,234,567.89"

<details> <summary>参考答案</summary>

6.1

python
s = "Hello, World!"
s[::-1]           # '!dlroW ,olleH'
s.upper()         # 'HELLO, WORLD!'
s[7:12]           # 'World'
s[:5]             # 'Hello'
s.count("l")      # 3

6.2

python
raw = " 张三, 李四 ,王五 "
names = [name.strip() for name in raw.split(",")]
print(names)   # ['张三', '李四', '王五']

(列表推导式见第 10 章,现在先记住这个惯用法)

6.3

python
text = input("输入一句话:")
cleaned = text.lower().replace(" ", "")
if cleaned == cleaned[::-1]:
    print("是回文")
else:
    print("不是回文")

6.4

python
path = "/home/user/documents/report.pdf"
filename = path.split("/")[-1]        # 'report.pdf'
ext = filename.split(".")[-1]         # 'pdf'

不过实际项目里应该用 pathlib(第 15 章):

python
from pathlib import Path
p = Path(path)
p.name      # 'report.pdf'
p.suffix    # '.pdf'
p.stem      # 'report'

6.5

python
f"{1234567.891:,.2f}"    # '1,234,567.89'

</details>


7. 流程控制

7.1 if / elif / else

python
age = 18

if age >= 18:
    print("成年")
elif age >= 13:
    print("青少年")
else:
    print("儿童")

要点:

  • elifelse if 的缩写,可以有任意多个
  • else 可选,最多一个
  • 从上往下,第一个为真的分支执行,其余全部跳过

嵌套(能不嵌套就不嵌套):

python
if is_logged_in:
    if is_admin:
        print("管理员面板")
    else:
        print("用户面板")
else:
    print("请登录")

💡 提示:用「卫语句」减少嵌套

python
# ❌ 嵌套三层
def process(user):
    if user is not None:
        if user.is_active:
            if user.has_permission:
                do_work()

# ✅ 提前返回,扁平化
def process(user):
    if user is None:
        return
    if not user.is_active:
        return
    if not user.has_permission:
        return
    do_work()

7.2 三元表达式

python
status = "成年" if age >= 18 else "未成年"

结构是 A if 条件 else B——顺序和其他语言的 条件 ? A : B 不同,读起来更像英语。

只适合简单情况,嵌套三元表达式请立刻改成 if/elif。

7.3 for 循环

Python 的 for遍历循环,不是 C 那种计数循环。

python
for item in [1, 2, 3]:
    print(item)

for char in "abc":
    print(char)

for key in {"a": 1, "b": 2}:
    print(key)      # 遍历字典默认得到键

range():生成数字序列

python
range(5)         # 0, 1, 2, 3, 4        (含头不含尾)
range(2, 5)      # 2, 3, 4
range(0, 10, 2)  # 0, 2, 4, 6, 8
range(5, 0, -1)  # 5, 4, 3, 2, 1
python
for i in range(5):
    print(i)       # 0 1 2 3 4

range 不会一次生成所有数字,它是惰性的——range(10**9) 几乎不占内存。要看内容需要转成列表:list(range(5))

enumerate():同时拿到索引和值

python
fruits = ["苹果", "香蕉", "橙子"]

# ❌ C 语言式写法
for i in range(len(fruits)):
    print(i, fruits[i])

# ✅ Python 式写法
for i, fruit in enumerate(fruits):
    print(i, fruit)

# 从 1 开始编号
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")

zip():并行遍历多个序列

python
names = ["张三", "李四"]
ages = [25, 30]

for name, age in zip(names, ages):
    print(f"{name}: {age}")

⚠️ zip最短的为准。要求长度必须一致时用 zip(a, b, strict=True)(Python 3.10+),长度不一致会报错——这通常是你想要的行为。

reversed() / sorted()

python
for x in reversed([1, 2, 3]):     # 3 2 1
    ...
for x in sorted([3, 1, 2]):       # 1 2 3
    ...

7.4 while 循环

条件为真时反复执行:

python
count = 0
while count < 5:
    print(count)
    count += 1

什么时候用 while 而不是 for:循环次数事先不确定时。

python
# 一直问到输入合法为止
while True:
    answer = input("y/n? ").lower()
    if answer in ("y", "n"):
        break
    print("请输入 y 或 n")

⚠️ 坑:死循环。忘记更新条件变量是最常见的错误:

python
count = 0
while count < 5:
    print(count)     # 忘了 count += 1 → 永远打印 0

卡住了按 Ctrl-C 中断。

7.5 break / continue / else

python
for i in range(10):
    if i == 3:
        continue      # 跳过本次剩余部分,进入下一轮
    if i == 6:
        break         # 直接结束整个循环
    print(i)
# 输出:0 1 2 4 5

循环的 else 子句(Python 特有,不常用但很好用):循环正常结束(没被 break)时执行。

python
for item in items:
    if item.is_target:
        print("找到了")
        break
else:
    print("遍历完了也没找到")

记忆法:把它读作 "for ... else" = "没找到就 else"。

⚠️ break 只跳出最内层循环。要跳出多层,要么用标志变量,要么把循环提取成函数用 return

python
def find_pair(matrix, target):
    for row in matrix:
        for x in row:
            if x == target:
                return True     # 直接跳出所有层
    return False

7.6 match 语句(Python 3.10+)

结构化模式匹配,比 switch 强大得多:

python
def handle(command):
    match command.split():
        case ["quit"]:
            return "退出"
        case ["go", direction]:
            return f"走向 {direction}"
        case ["drop", *items]:
            return f"丢下 {len(items)} 件物品"
        case _:
            return "看不懂"

也能匹配字典和对象:

python
match response:
    case {"status": 200, "data": data}:
        print(f"成功:{data}")
    case {"status": 404}:
        print("找不到")
    case {"status": code} if code >= 500:
        print(f"服务器错误 {code}")
    case _:
        print("未知响应")

⚠️ case 里的裸变量名是绑定不是比较

python
STATUS_OK = 200
match code:
    case STATUS_OK:      # ❌ 这会把 code 赋给 STATUS_OK,永远匹配!
        ...
    case Status.OK:      # ✅ 带点的名字才是值比较
        ...

简单的值分派用字典通常更直观:

python
handlers = {"start": do_start, "stop": do_stop}
handlers.get(command, do_unknown)()

7.7 pass / ... 占位

python
def todo():
    pass          # 语法上需要一个语句,但还没想好写什么

class Empty:
    ...           # Ellipsis,效果一样,类型标注场景常见

7.8 综合示例:猜数字游戏

把这一部分学的东西串起来:

python
import random

secret = random.randint(1, 100)
attempts = 0
MAX_ATTEMPTS = 7

print(f"我想了一个 1-100 的数,你有 {MAX_ATTEMPTS} 次机会")

while attempts < MAX_ATTEMPTS:
    raw = input(f"第 {attempts + 1} 次猜:")

    if not raw.isdigit():
        print("请输入数字")
        continue          # 输入非法不算次数

    guess = int(raw)
    attempts += 1

    if guess == secret:
        print(f"猜对了!用了 {attempts} 次")
        break
    elif guess < secret:
        print("小了")
    else:
        print("大了")
else:
    print(f"次数用完了,答案是 {secret}")

这个程序用到了:whileif/elif/elsecontinuebreakwhile...else、f-string、类型转换、标准库调用。

7.9 本章练习

练习 7.1 打印 1 到 100 中所有能被 3 整除但不能被 5 整除的数。

练习 7.2 经典 FizzBuzz:打印 1 到 30,能被 3 整除输出 Fizz,被 5 整除输出 Buzz,同时被 3 和 5 整除输出 FizzBuzz,否则输出数字本身。

练习 7.3 用循环打印一个 5 行的三角形:

*
**
***
****
*****

练习 7.4 输入一个数,判断是否是质数。

练习 7.5while 实现:不断让用户输入数字,输入空行时结束,最后输出总和与平均值。

练习 7.6 打印九九乘法表。

<details> <summary>参考答案</summary>

7.1

python
for n in range(1, 101):
    if n % 3 == 0 and n % 5 != 0:
        print(n)

7.2

python
for n in range(1, 31):
    if n % 15 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

注意判断顺序:必须先判断 15,否则永远到不了 FizzBuzz 分支。

7.3

python
for i in range(1, 6):
    print("*" * i)

7.4

python
n = int(input("输入一个数:"))

if n < 2:
    print("不是质数")
else:
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            print(f"不是质数,能被 {i} 整除")
            break
    else:
        print("是质数")

只需检查到 √n 是关键优化:如果 n = a × b 且 a ≤ b,那么 a 一定 ≤ √n。

7.5

python
numbers = []
while True:
    raw = input("输入数字(空行结束):")
    if raw == "":
        break
    numbers.append(float(raw))

if numbers:
    print(f"总和:{sum(numbers)}")
    print(f"平均:{sum(numbers) / len(numbers)}")
else:
    print("没有输入任何数字")

注意最后的 if numbers: —— 不判断的话空列表会导致除零错误。

7.6

python
for i in range(1, 10):
    for j in range(1, i + 1):
        print(f"{j}×{i}={i*j}", end="\t")
    print()

</details>


第 3 部分 · 数据结构

前面处理的都是单个值。真实程序需要成批地组织数据——这就是数据结构的作用。Python 内置了四种核心容器:列表、元组、字典、集合。选对容器,代码会短一半、快十倍。

8. 列表与元组

8.1 列表:有序、可变

python
fruits = ["苹果", "香蕉", "橙子"]
numbers = [1, 2, 3]
mixed = [1, "a", 3.14, True, None, [1, 2]]    # 可以混装任何类型
empty = []

访问与切片(规则和字符串完全一致):

python
fruits[0]      # '苹果'
fruits[-1]     # '橙子'
fruits[1:]     # ['香蕉', '橙子']
fruits[::-1]   # ['橙子', '香蕉', '苹果']

修改(这是列表和字符串最大的区别——列表可变):

python
fruits[0] = "梨"
fruits[1:3] = ["葡萄"]      # 切片赋值:用一个元素替换两个

8.2 增删改查

python
lst = [1, 2, 3]

# 增
lst.append(4)             # [1, 2, 3, 4]        末尾加一个
lst.extend([5, 6])        # [1, 2, 3, 4, 5, 6]  末尾加多个
lst.insert(0, 0)          # [0, 1, 2, ...]      指定位置插入
lst += [7]                # 等价于 extend

# 删
lst.remove(3)             # 删除第一个值为 3 的元素,没有则抛 ValueError
x = lst.pop()             # 删除并返回最后一个
x = lst.pop(0)            # 删除并返回索引 0 的元素
del lst[0]                # 按索引删
del lst[1:3]              # 按切片删
lst.clear()               # 清空

# 查
lst.index(2)              # 2 第一次出现的索引,没有则抛 ValueError
lst.count(2)              # 2 出现的次数
2 in lst                  # True

# 改(排序、反转)
lst.sort()                # 原地排序,返回 None
lst.sort(reverse=True)    # 降序
lst.reverse()             # 原地反转

⚠️ 坑一append vs extend

python
a = [1, 2]
a.append([3, 4])    # [1, 2, [3, 4]]    ← 把整个列表当一个元素塞进去
b = [1, 2]
b.extend([3, 4])    # [1, 2, 3, 4]      ← 逐个加入

⚠️ 坑二:原地方法返回 None

python
lst = [3, 1, 2]
result = lst.sort()      # result 是 None!
print(result)            # None

sort()reverse()append()原地修改的方法都返回 None。要拿到新列表用 sorted() / reversed()

python
new = sorted(lst)         # ✅ 返回新列表,原列表不变
new = list(reversed(lst)) # ✅

记忆法:动词原形是原地修改(sort),带 -ed 的是返回新对象(sorted)

⚠️ 坑三:边遍历边删除

python
lst = [1, 2, 3, 4]
for x in lst:
    if x % 2 == 0:
        lst.remove(x)     # ❌ 结果是 [1, 3, 4],2 被删后索引错位跳过了 3

正确做法——创建新列表:

python
lst = [x for x in lst if x % 2 != 0]     # ✅

或者倒着遍历(如果必须原地改):

python
for i in range(len(lst) - 1, -1, -1):
    if lst[i] % 2 == 0:
        del lst[i]

8.3 排序详解

python
words = ["banana", "Apple", "cherry"]

sorted(words)                          # ['Apple', 'banana', 'cherry']  大写在前
sorted(words, key=str.lower)           # ['Apple', 'banana', 'cherry']  忽略大小写
sorted(words, key=len)                 # 按长度
sorted(words, reverse=True)            # 降序

key 参数接收一个函数,对每个元素调用它,用返回值来比较。这是 Python 排序最强大的地方:

python
people = [
    {"name": "张三", "age": 30},
    {"name": "李四", "age": 25},
    {"name": "王五", "age": 30},
]

# 按年龄排序
sorted(people, key=lambda p: p["age"])

# 多级排序:先按年龄,年龄相同按名字
sorted(people, key=lambda p: (p["age"], p["name"]))

# 年龄降序,名字升序(技巧:数值取负)
sorted(people, key=lambda p: (-p["age"], p["name"]))

💡 sorted稳定排序——相等元素保持原有相对顺序。所以多级排序也可以分步做(先按次要键排,再按主要键排)。

operator.itemgetter 比 lambda 更快也更清晰:

python
from operator import itemgetter
sorted(people, key=itemgetter("age", "name"))

8.4 元组:有序、不可变

python
point = (3, 4)
single = (42,)        # ⚠️ 单元素元组必须有逗号!(42) 只是加了括号的整数
empty = ()
no_paren = 3, 4       # 括号可省略,逗号才是关键

元组不能修改:

python
point[0] = 5     # TypeError

什么时候用元组而不是列表

  1. 数据不该被改 —— 坐标、RGB 颜色、数据库一行记录
  2. 要当字典的键 —— 列表不可哈希,元组可以
python
locations = {(0, 0): "原点", (1, 1): "对角"}   # ✅ 元组做键
locations = {[0, 0]: "原点"}                  # ❌ TypeError: unhashable type
  1. 函数返回多个值 —— 实际上返回的就是元组
python
def divmod_(a, b):
    return a // b, a % b     # 返回元组

q, r = divmod_(17, 5)        # 3, 2

8.5 解包(Unpacking)

这是 Python 里极其常用的语法糖:

python
a, b = 1, 2               # a=1, b=2
a, b = b, a               # 交换,不需要临时变量

x, y, z = [1, 2, 3]       # 列表也能解包
name, age = ("张三", 25)

# 星号收集剩余部分
first, *rest = [1, 2, 3, 4]      # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4]       # init=[1, 2, 3], last=4
a, *mid, z = [1, 2, 3, 4, 5]     # a=1, mid=[2,3,4], z=5

# 忽略不需要的值,约定用下划线
_, important, _ = (1, 2, 3)

# 嵌套解包
(a, b), c = (1, 2), 3

解包在循环里特别好用:

python
pairs = [(1, "a"), (2, "b")]
for num, letter in pairs:
    print(num, letter)

⚠️ 数量不匹配会报错:

python
a, b = [1, 2, 3]     # ValueError: too many values to unpack

8.6 引用语义:最容易踩的坑

前面提到过,变量是标签不是盒子。对可变对象(列表、字典、集合)来说,这个区别至关重要。

python
a = [1, 2, 3]
b = a            # b 和 a 指向同一个列表
b.append(4)
print(a)         # [1, 2, 3, 4]  ← a 也变了!

复制列表的方式

python
b = a.copy()        # 推荐
b = a[:]            # 切片复制
b = list(a)         # 构造新列表

但这三种都是浅拷贝——只复制最外层:

python
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)      # [[1, 2, 99], [3, 4]]  ← 内层还是共享的!

嵌套结构要深拷贝

python
import copy
b = copy.deepcopy(a)     # 递归复制所有层

⚠️ *坑:` ` 复制嵌套列表**

python
grid = [[0] * 3] * 3        # ❌ 三行指向同一个列表
grid[0][0] = 1
print(grid)                  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

grid = [[0] * 3 for _ in range(3)]   # ✅ 每行都是新列表

8.7 常用内置函数

python
nums = [3, 1, 4, 1, 5, 9, 2, 6]

len(nums)         # 8
sum(nums)         # 31
max(nums)         # 9
min(nums)         # 1
sorted(nums)      # [1, 1, 2, 3, 4, 5, 6, 9]
any([0, 1, 0])    # True   有任意一个真
all([1, 1, 0])    # False  全部为真才真
list(reversed(nums))

# max/min 也支持 key
max(words, key=len)                 # 最长的词
max(people, key=lambda p: p["age"]) # 年龄最大的人

# sum 可以指定起始值
sum([[1], [2]], [])                 # [1, 2]  (拼接列表,但效率低)

💡 anyall 对空序列的行为:any([])Falseall([])True("空集合中所有元素都满足条件",数学上叫空真)。这偶尔会咬人。

8.8 二维列表

python
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

matrix[1][2]        # 6   第 2 行第 3 列

# 遍历
for row in matrix:
    for value in row:
        print(value, end=" ")
    print()

# 转置(行列互换)
transposed = list(zip(*matrix))    # [(1,4,7), (2,5,8), (3,6,9)]

zip(*matrix) 里的 *解包传参:把 matrix 的每一行作为独立参数传给 zip。这是个很地道的技巧。

💡 大量数值计算请用 numpy,它的二维数组比嵌套列表快几十倍,语法也更方便。

8.9 本章练习

练习 8.1 给定 nums = [3, 7, 1, 9, 4, 1, 7]

  1. 求最大值、最小值、平均值
  2. 去重后排序
  3. 找出所有大于 3 的数
  4. 反转列表(两种方法)

练习 8.2 下面代码输出什么?为什么?

python
a = [1, 2, 3]
b = a
c = a[:]
a.append(4)
print(b, c)

练习 8.3 写一个程序,把 [[1,2],[3,4],[5,6]] 展平成 [1,2,3,4,5,6]

练习 8.4 给定学生成绩 scores = [("张三", 85), ("李四", 92), ("王五", 78)],按成绩从高到低排序并输出排名。

练习 8.5 实现一个"移除列表中所有重复元素但保持原顺序"的函数。

练习 8.6 为什么 [[0]*3]*3 有问题?写出正确的创建 3×3 零矩阵的方法。

<details> <summary>参考答案</summary>

8.1

python
nums = [3, 7, 1, 9, 4, 1, 7]

max(nums)                      # 9
min(nums)                      # 1
sum(nums) / len(nums)          # 4.571428571428571

sorted(set(nums))              # [1, 3, 4, 7, 9]

[n for n in nums if n > 3]     # [7, 9, 4, 7]

nums[::-1]                     # 切片,返回新列表
list(reversed(nums))           # 同上
nums.reverse()                 # 原地反转,返回 None

8.2 输出 [1, 2, 3, 4] [1, 2, 3]b = a 只是多贴了个标签,指向同一个列表,所以 a 变 b 也变。c = a[:] 创建了新列表,所以 c 不受影响。

8.3

python
nested = [[1, 2], [3, 4], [5, 6]]

# 方法 1:推导式(最常用)
flat = [x for row in nested for x in row]

# 方法 2:itertools
from itertools import chain
flat = list(chain.from_iterable(nested))

# 方法 3:循环
flat = []
for row in nested:
    flat.extend(row)

8.4

python
scores = [("张三", 85), ("李四", 92), ("王五", 78)]
ranked = sorted(scores, key=lambda s: s[1], reverse=True)

for rank, (name, score) in enumerate(ranked, start=1):
    print(f"第 {rank} 名:{name} {score} 分")

8.5

python
def dedup(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

Python 3.7+ 字典保持插入顺序,所以还有个一行写法:

python
def dedup(items):
    return list(dict.fromkeys(items))

8.6 [[0]*3]*3 创建的是三个指向同一个列表的引用,改一行会三行全改。

python
grid = [[0] * 3 for _ in range(3)]   # ✅

</details>


9. 字典与集合

9.1 字典:键值对映射

python
person = {
    "name": "张三",
    "age": 25,
    "city": "北京",
}

empty = {}
from_pairs = dict([("a", 1), ("b", 2)])
from_kwargs = dict(name="张三", age=25)

访问

python
person["name"]              # '张三'
person["email"]             # ❌ KeyError!

person.get("email")         # None,不报错
person.get("email", "无")   # '无',指定默认值

💡 原则:确定键存在时用 [](键不存在应该报错的场景);不确定时用 .get()

修改与添加

python
person["age"] = 26              # 修改
person["email"] = "[email protected]"     # 键不存在则添加

person.update({"age": 27, "phone": "138..."})   # 批量更新
person |= {"age": 28}                          # Python 3.9+ 的合并写法

删除

python
del person["email"]
age = person.pop("age")             # 删除并返回值
age = person.pop("age", None)       # 键不存在时返回默认值,不报错
key, val = person.popitem()         # 删除并返回最后插入的一对
person.clear()

遍历

python
for key in person:                  # 默认遍历键
    print(key)

for key in person.keys():           # 显式,等价
    ...
for value in person.values():
    ...
for key, value in person.items():   # 最常用
    print(f"{key}: {value}")

检查存在

python
"name" in person          # True   —— 查的是键
"张三" in person.values() # True   —— 查值需要显式指定

9.2 字典的特点

有序:Python 3.7 起,字典保证按插入顺序遍历。(3.6 是实现细节,3.7 起写进语言规范。)

键必须可哈希:字符串、数字、元组、frozenset 可以;列表、字典、集合不行。

python
d = {[1, 2]: "x"}     # TypeError: unhashable type: 'list'
d = {(1, 2): "x"}     # ✅

查找是 O(1):不管字典多大,按键查找都几乎是瞬时的。这是字典最大的价值。

9.3 常用字典技巧

统计频率

python
text = "hello world"
counts = {}
for char in text:
    counts[char] = counts.get(char, 0) + 1

collections.Counter 更简单:

python
from collections import Counter
counts = Counter(text)
counts.most_common(3)       # 出现最多的 3 个 [(‘l', 3), ('o', 2), ...]

分组

python
from collections import defaultdict

words = ["apple", "avocado", "banana", "blueberry"]
groups = defaultdict(list)
for word in words:
    groups[word[0]].append(word)
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry']}

defaultdict(list) 在键不存在时自动创建空列表,省去了 if key not in d 的判断。

也可以用 setdefault(普通字典就能用):

python
groups = {}
for word in words:
    groups.setdefault(word[0], []).append(word)

合并字典

python
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}

merged = {**a, **b}       # {'x': 1, 'y': 20, 'z': 3}  后者覆盖前者
merged = a | b            # Python 3.9+,同上,更清晰

反转键值

python
inverted = {v: k for k, v in d.items()}

字典推导式

python
squares = {n: n**2 for n in range(5)}        # {0:0, 1:1, 2:4, 3:9, 4:16}
filtered = {k: v for k, v in d.items() if v > 10}

嵌套字典

python
config = {
    "database": {"host": "localhost", "port": 5432},
    "cache": {"ttl": 300},
}

config["database"]["host"]                      # 'localhost'
config.get("cache", {}).get("size", 100)        # 安全地取深层值

9.4 集合:无序、不重复

python
s = {1, 2, 3}
s = set([1, 2, 2, 3])       # {1, 2, 3}  自动去重
empty = set()               # ⚠️ 不能写 {},那是空字典

基本操作

python
s.add(4)
s.remove(4)          # 不存在则 KeyError
s.discard(4)         # 不存在也不报错
s.pop()              # 任意弹出一个(哪个不确定,也别指望是随机的)
s.clear()
len(s)
3 in s               # O(1),非常快

集合运算

python
a = {1, 2, 3}
b = {2, 3, 4}

a | b        # {1, 2, 3, 4}   并集     a.union(b)
a & b        # {2, 3}         交集     a.intersection(b)
a - b        # {1}            差集     a.difference(b)
a ^ b        # {1, 4}         对称差   a.symmetric_difference(b)

a <= b       # 是否是子集      a.issubset(b)
a >= b       # 是否是超集
a.isdisjoint(b)   # 是否没有交集

集合的两大用途

  1. 去重
python
unique = list(set(items))          # 但会打乱顺序
unique = list(dict.fromkeys(items)) # 保持顺序
  1. 快速成员检查
python
# ❌ 列表查找是 O(n)
banned = ["a", "b", "c", ...]      # 10000 个
if word in banned:  ...             # 慢

# ✅ 集合查找是 O(1)
banned = {"a", "b", "c", ...}
if word in banned:  ...             # 快

frozenset:不可变的集合,可以做字典的键。

python
fs = frozenset([1, 2, 3])
d = {fs: "value"}     # ✅

9.5 四种容器怎么选

需求选什么
有序、可增删改列表 list
有序、不可变、当字典键元组 tuple
键值映射、按键快速查找字典 dict
去重、快速判断存在、集合运算集合 set
操作listtupledictset
有序✅(插入序)
可变
允许重复键不可重复
索引访问O(1)O(1)按键 O(1)
in 查找O(n)O(n)O(1)O(1)
可作字典键

9.6 collections 模块

标准库提供了几个更专门的容器:

python
from collections import Counter, defaultdict, deque, namedtuple, ChainMap

# Counter —— 计数
c = Counter("mississippi")
c.most_common(2)          # [('i', 4), ('s', 4)]
c["i"]                    # 4
c["z"]                    # 0(不报错)

# defaultdict —— 带默认值的字典
d = defaultdict(int)      # 默认 0
d["x"] += 1               # 不需要先初始化
d = defaultdict(list)     # 默认空列表

# deque —— 双端队列,两头增删都是 O(1)
q = deque([1, 2, 3])
q.appendleft(0)           # 列表的 insert(0, x) 是 O(n),deque 是 O(1)
q.popleft()
q = deque(maxlen=5)       # 固定长度,超出自动丢弃最老的

# namedtuple —— 带名字的元组(更推荐用 dataclass,见第 18 章)
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x                       # 3

9.7 本章练习

练习 9.1 给定一段文本,统计每个单词出现的次数,输出前 5 个最常见的。

练习 9.2 有两个列表 a = [1,2,3,4,5]b = [4,5,6,7],求:

  1. 同时在两个列表中的元素
  2. 只在 a 中的元素
  3. 在任一列表中的所有元素(去重)

练习 9.3 给定 students = [{"name":"张三","class":"A"}, {"name":"李四","class":"B"}, {"name":"王五","class":"A"}],按班级分组,输出 {"A": ["张三","王五"], "B": ["李四"]}

练习 9.4 判断两个字符串是否是变位词(字母相同、顺序不同,如 "listen" 和 "silent")。

练习 9.5 下面代码为什么报错?怎么改?

python
d = {}
d[[1, 2]] = "value"

练习 9.6 写一个函数,接收一个嵌套字典和一个用点分隔的路径字符串(如 "database.host"),返回对应的值,路径不存在时返回 None。

<details> <summary>参考答案</summary>

9.1

python
from collections import Counter

text = "the quick brown fox jumps over the lazy dog the end"
words = text.lower().split()
counts = Counter(words)

for word, count in counts.most_common(5):
    print(f"{word}: {count}")

9.2

python
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7}

a & b       # {4, 5}
a - b       # {1, 2, 3}
a | b       # {1, 2, 3, 4, 5, 6, 7}

9.3

python
from collections import defaultdict

students = [
    {"name": "张三", "class": "A"},
    {"name": "李四", "class": "B"},
    {"name": "王五", "class": "A"},
]

groups = defaultdict(list)
for s in students:
    groups[s["class"]].append(s["name"])

print(dict(groups))   # {'A': ['张三', '王五'], 'B': ['李四']}

也可以用 itertools.groupby(标准库老成员,不是新特性),但它只对相邻的相同键分组,用之前必须先按同一个键排序,不如上面直观。

9.4

python
def is_anagram(a, b):
    return sorted(a.lower()) == sorted(b.lower())

# 或者用 Counter,语义更清晰
from collections import Counter
def is_anagram(a, b):
    return Counter(a.lower()) == Counter(b.lower())

注意不能用 set(a) == set(b)——那样 "aab" 和 "abb" 会被判为变位词。

9.5 列表不可哈希,不能做字典的键。改用元组:

python
d[(1, 2)] = "value"

9.6

python
def get_nested(data, path, default=None):
    current = data
    for key in path.split("."):
        if not isinstance(current, dict) or key not in current:
            return default
        current = current[key]
    return current

config = {"database": {"host": "localhost", "port": 5432}}
get_nested(config, "database.host")      # 'localhost'
get_nested(config, "database.user")      # None
get_nested(config, "cache.ttl", 300)     # 300

</details>


10. 推导式

推导式(comprehension)是 Python 最有辨识度的语法之一。它把"创建一个新集合"这件事压缩成一行。

10.1 列表推导式

基本形式

python
[表达式 for 变量 in 可迭代对象]
python
# 传统写法
squares = []
for n in range(5):
    squares.append(n ** 2)

# 推导式
squares = [n ** 2 for n in range(5)]      # [0, 1, 4, 9, 16]

带过滤

python
[表达式 for 变量 in 可迭代对象 if 条件]
python
evens = [n for n in range(20) if n % 2 == 0]
long_words = [w for w in words if len(w) > 5]
valid = [int(x) for x in inputs if x.isdigit()]

带三元表达式(注意位置不同!):

python
# if 在后面 = 过滤(决定要不要这个元素)
[n for n in nums if n > 0]

# if...else 在前面 = 转换(每个元素都要,只是值不同)
[n if n > 0 else 0 for n in nums]

这是初学者最常混淆的地方。记住:`for` 前面的部分是"生成什么",`for` 后面的 `if` 是"要不要"

多层循环

python
# 展平嵌套列表
flat = [x for row in matrix for x in row]

# 等价于
flat = []
for row in matrix:
    for x in row:
        flat.append(x)

注意 for 的顺序和嵌套循环的书写顺序一致(外层在前)。

笛卡尔积

python
pairs = [(a, b) for a in "AB" for b in [1, 2]]
# [('A', 1), ('A', 2), ('B', 1), ('B', 2)]

10.2 字典推导式与集合推导式

python
# 字典推导式
{k: v for ...}
squares = {n: n**2 for n in range(5)}
inverted = {v: k for k, v in d.items()}
upper_keys = {k.upper(): v for k, v in d.items()}
filtered = {k: v for k, v in d.items() if v is not None}

# 集合推导式
{表达式 for ...}
unique_lengths = {len(w) for w in words}

10.3 生成器表达式

把方括号换成圆括号,得到的是生成器——惰性求值,不占内存:

python
squares_list = [n**2 for n in range(1000000)]    # 立刻创建 100 万个元素,占几十 MB
squares_gen = (n**2 for n in range(1000000))     # 几乎不占内存,用一个算一个

作为函数的唯一参数时,括号可以省略:

python
sum(n**2 for n in range(100))          # 不需要写成 sum((n**2 for ...))
any(x > 100 for x in nums)
max(len(w) for w in words)
"\n".join(str(x) for x in nums)

💡 什么时候用生成器表达式:只遍历一次、数据量大、或者只需要聚合结果时。需要多次访问、需要索引、需要 len() 时用列表。

第 19 章会详细讲生成器。

10.4 什么时候不该用推导式

推导式很爽,但滥用会写出没人看得懂的代码。

❌ 太复杂

python
result = [transform(x) if cond1(x) else other(x)
          for sublist in data if check(sublist)
          for x in sublist if x is not None and validate(x)]

这种情况老老实实写循环,可读性远好于炫技。

❌ 只是为了副作用

python
[print(x) for x in items]     # ❌ 创建了一个没人要的 None 列表
for x in items: print(x)      # ✅

判断标准:一行写不下,或者需要停下来想一秒才能看懂,就改成循环。

10.5 常见模式速查

python
# 转换类型
[int(x) for x in strings]

# 过滤 None
[x for x in items if x is not None]

# 提取字段
[p["name"] for p in people]

# 字符串处理
[line.strip() for line in lines if line.strip()]

# 索引 + 值
[f"{i}: {v}" for i, v in enumerate(items)]

# 条件转换
[x if x > 0 else 0 for x in nums]

# 两个列表配对
[(a, b) for a, b in zip(list1, list2)]

# 嵌套展平
[x for sub in nested for x in sub]

# 构建查找字典
{item["id"]: item for item in items}

# 反转字典
{v: k for k, v in d.items()}

# 去重(无序)
{x for x in items}

10.6 本章练习

练习 10.1 用推导式完成:

  1. 1 到 20 中所有偶数的平方
  2. 一个句子里所有长度大于 3 的单词,转成大写
  3. ["1", "a", "2", "b", "3"] 中能转成数字的转成整数,其余丢弃
  4. 生成一个 3×3 的乘法表(嵌套列表)

练习 10.2 下面两行有什么区别?

python
[x for x in range(10) if x % 2 == 0]
[x if x % 2 == 0 else None for x in range(10)]

练习 10.3 给定 data = {"a": 1, "b": None, "c": 3, "d": None},用字典推导式去掉值为 None 的项。

练习 10.4 用一行代码求 1 到 100 中所有能被 7 整除的数的和。

练习 10.5 给定一个二维列表,用推导式实现转置(不用 zip)。

<details> <summary>参考答案</summary>

10.1

python
# 1
[n**2 for n in range(1, 21) if n % 2 == 0]

# 2
sentence = "the quick brown fox jumps"
[w.upper() for w in sentence.split() if len(w) > 3]

# 3
items = ["1", "a", "2", "b", "3"]
[int(x) for x in items if x.isdigit()]

# 4
[[i * j for j in range(1, 4)] for i in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

10.2

  • 第一行是过滤,结果有 5 个元素:[0, 2, 4, 6, 8]
  • 第二行是转换,结果有 10 个元素:[0, None, 2, None, 4, ...]

10.3

python
data = {"a": 1, "b": None, "c": 3, "d": None}
cleaned = {k: v for k, v in data.items() if v is not None}
# {'a': 1, 'c': 3}

10.4

python
sum(n for n in range(1, 101) if n % 7 == 0)    # 735

用生成器表达式而不是列表推导式——不需要中间列表。

10.5

python
matrix = [[1, 2, 3], [4, 5, 6]]
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
# [[1, 4], [2, 5], [3, 6]]

</details>


第 4 部分 · 组织代码

11. 函数

程序长到几十行以后,就需要把它拆成块。函数是最基本的拆分单位。

11.1 定义与调用

python
def greet(name):
    """打招呼。"""            # 文档字符串(docstring)
    return f"你好,{name}"

message = greet("张三")
print(message)                # 你好,张三
  • def 开始定义
  • 括号里是形参(parameter)
  • return 返回一个值给调用方
  • 没有 return 时函数返回 None

return 会立刻结束函数:

python
def check(n):
    if n < 0:
        return "负数"
    return "非负数"
    print("永远不会执行")     # 死代码

11.2 参数

位置参数

python
def power(base, exponent):
    return base ** exponent

power(2, 3)      # 8   按位置对应

默认参数

python
def greet(name, greeting="你好"):
    return f"{greeting},{name}"

greet("张三")                    # 你好,张三
greet("张三", "早上好")           # 早上好,张三

默认参数必须放在非默认参数后面

python
def f(a=1, b): ...       # ❌ SyntaxError

⚠️ 重要的坑:可变默认参数

python
def add_item(item, items=[]):    # ❌ 危险!
    items.append(item)
    return items

add_item("a")     # ['a']
add_item("b")     # ['a', 'b']   ← 竟然记住了上次的!

原因:默认值只在函数定义时求值一次,之后所有调用共享同一个列表。

正确写法:

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

规则:默认参数只能用不可变对象(数字、字符串、元组、None)。

关键字参数

调用时用 名字=值,顺序可以打乱:

python
power(exponent=3, base=2)    # 8

💡 布尔参数强烈建议用关键字形式,可读性天差地别:

python
save(data, True, False)                    # ❌ True 和 False 是什么?
save(data, overwrite=True, backup=False)   # ✅

可变参数 args 和 kwargs

python
def total(*numbers):           # 收集任意多个位置参数,得到元组
    return sum(numbers)

total(1, 2, 3)                 # 6
total(1, 2, 3, 4, 5)           # 15


def config(**options):         # 收集任意多个关键字参数,得到字典
    for key, value in options.items():
        print(f"{key} = {value}")

config(debug=True, port=8080)


def flexible(a, b=2, *args, **kwargs):
    print(a, b, args, kwargs)

flexible(1, 2, 3, 4, x=5)      # 1 2 (3, 4) {'x': 5}

名字 args/kwargs 只是约定,关键是 ***

解包传参*** 的另一面):

python
nums = [1, 2, 3]
total(*nums)                   # 等价于 total(1, 2, 3)

opts = {"debug": True, "port": 8080}
config(**opts)                 # 等价于 config(debug=True, port=8080)

强制位置参数 / 强制关键字参数

python
def f(a, b, /, c, d, *, e, f):
    ...
#      ↑ / 之前必须按位置传
#              ↑ * 之后必须用关键字传
python
def create_user(name, *, admin=False, active=True):
    ...

create_user("张三", admin=True)      # ✅
create_user("张三", True)            # ❌ TypeError

* 强制关键字是很好的 API 设计习惯——防止调用方写出 create_user("张三", True, False) 这种看不懂的代码。

11.3 返回多个值

python
def min_max(numbers):
    return min(numbers), max(numbers)     # 实际返回一个元组

lo, hi = min_max([3, 1, 4])               # 解包
result = min_max([3, 1, 4])               # (1, 4)

返回值多于 3 个时,考虑返回字典或 dataclass(第 18 章),可读性更好。

11.4 文档字符串

python
def calculate_bmi(weight, height):
    """计算身体质量指数。

    Args:
        weight: 体重,单位公斤
        height: 身高,单位米

    Returns:
        BMI 值(float)

    Raises:
        ValueError: 身高为 0 或负数时
    """
    if height <= 0:
        raise ValueError("身高必须为正数")
    return weight / height ** 2

查看文档:

python
help(calculate_bmi)
calculate_bmi.__doc__

💡 一行函数不需要写这么详细的 docstring,一句话说清楚就行。但公开的 API 一定要写

11.5 lambda:匿名函数

python
square = lambda x: x ** 2      # 等价于 def square(x): return x ** 2

lambda 只能写一个表达式,不能有语句、不能有多行。

它真正的用武之地是当作参数传给别的函数

python
sorted(people, key=lambda p: p["age"])
list(filter(lambda x: x > 0, nums))
list(map(lambda x: x * 2, nums))

⚠️ 不要把 lambda 赋值给变量——那样还不如直接 def,因为 def 会给函数一个名字,报错信息更有用:

python
square = lambda x: x ** 2      # ❌ PEP 8 明确不推荐
def square(x): return x ** 2   # ✅

💡 很多用 lambda 的场景有更好的替代:

python
map(lambda x: x * 2, nums)         →  [x * 2 for x in nums]
filter(lambda x: x > 0, nums)      →  [x for x in nums if x > 0]
sorted(d, key=lambda x: x[1])      →  sorted(d, key=itemgetter(1))

11.6 函数是一等公民

在 Python 里,函数和数字、字符串一样,是可以传来传去的对象:

python
def double(x):
    return x * 2

f = double              # 赋值给变量(注意没有括号)
f(5)                    # 10

funcs = [double, abs, len]        # 放进列表
for fn in funcs:
    print(fn.__name__)

def apply(fn, value):             # 当参数传
    return fn(value)
apply(double, 5)                  # 10

def make_multiplier(n):           # 当返回值
    def multiplier(x):
        return x * n
    return multiplier

triple = make_multiplier(3)
triple(5)                         # 15

最后一个例子就是闭包,下一章详细讲。这个能力是装饰器(第 20 章)的基础。

11.7 递归

函数调用自己:

python
def factorial(n):
    if n <= 1:          # 基线条件(base case)—— 必须有!
        return 1
    return n * factorial(n - 1)

factorial(5)            # 120

递归必须有基线条件,否则会无限递归直到 RecursionError(Python 默认递归深度限制约 1000 层)。

递归适合处理天然递归的结构:树、嵌套目录、嵌套 JSON。

python
def total_size(item):
    """递归计算嵌套列表中所有数字的和。"""
    if isinstance(item, list):
        return sum(total_size(x) for x in item)
    return item

total_size([1, [2, [3, 4]], 5])    # 15

⚠️ 能用循环表达的,通常用循环更快也更安全。斐波那契的朴素递归实现是经典反例——fib(35) 要算好几秒,因为重复计算了指数级次数。用 functools.cache 可以一行修复:

python
from functools import cache

@cache
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

fib(100)     # 瞬间返回

11.8 写好函数的几条原则

1. 一个函数只做一件事

python
# ❌ 又读文件又计算又打印
def process():
    data = open("f.txt").read()
    result = complicated_math(data)
    print(result)

# ✅ 拆开,每个都可测试、可复用
def load_data(path): ...
def compute(data): ...
def report(result): ...

2. 函数名用动词

python
get_user()  calculate_tax()  is_valid()  has_permission()  send_email()

返回布尔值的函数用 is_ / has_ / can_ 开头。

3. 参数不超过 3-4 个

超过就说明该把相关参数打包成一个对象了。

4. 避免副作用,或者让副作用显而易见

python
# ❌ 名字说是"计算",实际上改了输入
def calculate_total(items):
    items.sort()          # 意外修改了调用方的列表!
    return sum(items)

# ✅ 不改输入
def calculate_total(items):
    return sum(sorted(items))

5. 早返回,减少嵌套(第 7 章的卫语句)

11.9 本章练习

练习 11.1 写一个函数 is_prime(n) 判断质数。

练习 11.2 写一个函数,接收任意多个数字,返回它们的平均值。空参数时返回 0。

练习 11.3 下面代码有 bug,找出来并修复:

python
def append_log(msg, logs=[]):
    logs.append(msg)
    return logs

练习 11.4 写一个函数 word_count(text),返回一个字典,统计每个单词出现次数。要求忽略大小写和标点。

练习 11.5 写一个函数 retry(func, times=3),调用 func(),如果抛异常就重试,最多 times 次,全失败则抛出最后一次的异常。(可以先跳过,学完第 14 章再回来做)

练习 11.6 用递归实现:计算一个嵌套字典的最大深度。

<details> <summary>参考答案</summary>

11.1

python
def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    for i in range(3, int(n ** 0.5) + 1, 2):
        if n % i == 0:
            return False
    return True

11.2

python
def average(*numbers):
    if not numbers:
        return 0
    return sum(numbers) / len(numbers)

average(1, 2, 3)     # 2.0
average()            # 0

11.3 可变默认参数的坑——所有调用共享同一个列表。

python
def append_log(msg, logs=None):
    if logs is None:
        logs = []
    logs.append(msg)
    return logs

11.4

python
import re
from collections import Counter

def word_count(text):
    words = re.findall(r"\w+", text.lower())
    return dict(Counter(words))

word_count("The cat. The DOG! the cat?")
# {'the': 3, 'cat': 2, 'dog': 1}

11.5

python
def retry(func, times=3):
    last_error = None
    for attempt in range(times):
        try:
            return func()
        except Exception as e:
            last_error = e
            print(f"第 {attempt + 1} 次失败:{e}")
    raise last_error

11.6

python
def max_depth(d):
    if not isinstance(d, dict) or not d:
        return 0
    return 1 + max(max_depth(v) for v in d.values())

max_depth({"a": {"b": {"c": 1}}})    # 3

</details>


12. 作用域与闭包

12.1 四层作用域:LEGB

Python 查找一个变量名时,按固定顺序找四个地方:

L (Local)      —— 当前函数内部
E (Enclosing)  —— 外层函数(嵌套函数才有)
G (Global)     —— 模块顶层
B (Built-in)   —— Python 内置(print、len 这些)
python
x = "global"                  # G

def outer():
    x = "enclosing"           # E
    def inner():
        x = "local"           # L
        print(x)              # local
    inner()
    print(x)                  # enclosing

outer()
print(x)                      # global

找到第一个就停止,找不到就 NameError

12.2 函数内修改外部变量

读取外部变量是允许的

python
counter = 0

def show():
    print(counter)    # ✅ 能读到

但赋值会创建新的局部变量

python
counter = 0

def increment():
    counter = counter + 1     # ❌ UnboundLocalError

报错原因:Python 看到函数里有 counter = 赋值,就把 counter 判定为局部变量;然后执行右边的 counter + 1 时,局部的 counter 还没有值。

解决方式

python
counter = 0

def increment():
    global counter            # 声明:我要改的是全局那个
    counter += 1

嵌套函数里改外层变量用 nonlocal

python
def outer():
    count = 0
    def inner():
        nonlocal count        # 改的是 outer 的 count,不是全局
        count += 1
    inner()
    inner()
    return count              # 2

⚠️ `global` 是代码异味。全局可变状态让程序难以理解、难以测试、难以并发。绝大多数情况应该改成:传参数进去,返回结果出来。

python
# ❌
total = 0
def add(x):
    global total
    total += x

# ✅
def add(total, x):
    return total + x

12.3 闭包

内层函数"记住"了外层函数的变量,即使外层函数已经执行完毕:

python
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

c1 = make_counter()
c1()    # 1
c1()    # 2
c1()    # 3

c2 = make_counter()
c2()    # 1  —— 独立的计数器,各自有各自的 count

make_counter 早已返回,但 count 还活着,因为 counter 函数持有对它的引用。这就是闭包。

典型用途:函数工厂

python
def make_validator(min_len):
    def validate(text):
        return len(text) >= min_len
    return validate

check_password = make_validator(8)
check_username = make_validator(3)

check_password("abc")      # False
check_username("abc")      # True

⚠️ 经典坑:循环中创建闭包

python
funcs = []
for i in range(3):
    funcs.append(lambda: i)

[f() for f in funcs]     # [2, 2, 2]  ← 都是 2!

原因:三个 lambda 引用的是同一个变量 i,而循环结束后 i 是 2。

修复——用默认参数在定义时"冻结"当前值:

python
for i in range(3):
    funcs.append(lambda i=i: i)

[f() for f in funcs]     # [0, 1, 2]  ✅

或者用工厂函数:

python
def make_f(i):
    return lambda: i
funcs = [make_f(i) for i in range(3)]

12.4 本章练习

练习 12.1 下面代码输出什么?

python
x = 10
def f():
    x = 20
    def g():
        nonlocal x
        x = 30
    g()
    print(x)
f()
print(x)

练习 12.2 修复这段代码:

python
total = 0
def add(n):
    total += n
    return total

练习 12.3 用闭包写一个"累加器":acc = make_accumulator()acc(10) 返回 10,acc(5) 返回 15,acc(3) 返回 18。

练习 12.4 解释为什么下面输出是 [2, 2, 2] 而不是 [0, 1, 2],并给出两种修复方法。

python
fs = [lambda: i for i in range(3)]
print([f() for f in fs])

<details> <summary>参考答案</summary>

12.1 输出 30 然后 10g 里的 nonlocal x 修改的是 f 的局部变量 x(从 20 变 30),全局的 x 不受影响。

12.2

python
# 方法 1:用 global(不推荐)
total = 0
def add(n):
    global total
    total += n
    return total

# 方法 2:改成纯函数(推荐)
def add(total, n):
    return total + n

# 方法 3:用闭包封装状态
def make_adder():
    total = 0
    def add(n):
        nonlocal total
        total += n
        return total
    return add

12.3

python
def make_accumulator():
    total = 0
    def accumulate(n):
        nonlocal total
        total += n
        return total
    return accumulate

acc = make_accumulator()
acc(10)    # 10
acc(5)     # 15
acc(3)     # 18

12.4 三个 lambda 引用的是同一个变量 i,而不是各自捕获了当时的值。等到真正调用 f() 时,推导式早已执行完毕,i 停在最后一个值 2 上,所以三次都返回 2。

关键点:闭包捕获的是变量,不是变量当时的

修复方法:

python
# 方法 1:默认参数捕获
fs = [lambda i=i: i for i in range(3)]

# 方法 2:工厂函数
def make(i):
    return lambda: i
fs = [make(i) for i in range(3)]

</details>


13. 模块与包

代码超过几百行就该拆文件了。Python 用模块和包来组织多文件项目。

13.1 模块 = 一个 .py 文件

创建 mathtools.py

python
PI = 3.14159

def circle_area(r):
    return PI * r ** 2

def circle_circumference(r):
    return 2 * PI * r

在同目录的另一个文件里使用:

python
import mathtools

mathtools.circle_area(2)     # 12.56636
mathtools.PI                 # 3.14159

13.2 import 的几种形式

python
import math                       # 导入整个模块
math.sqrt(16)

import math as m                  # 起别名
m.sqrt(16)

from math import sqrt             # 只导入某个名字
sqrt(16)

from math import sqrt, pi, floor  # 导入多个
from math import sqrt as square_root

from math import *                # ❌ 导入所有 —— 不要这样做

⚠️ from x import * 的问题:你不知道引入了哪些名字,可能悄悄覆盖你自己的变量,读代码的人也无法知道 sqrt 是从哪来的。唯一可接受的场景是交互式实验。

该用哪种

  • 模块名短、用得多 → import math,调用时带前缀最清晰
  • 只用一两个函数 → from math import sqrt
  • 名字太长或有冲突 → import numpy as np

13.3 包:装模块的文件夹

myproject/
├── main.py
└── utils/
    ├── __init__.py
    ├── text.py
    └── files.py
python
# main.py
from utils.text import clean
from utils import files

import utils.text as text

__init__.py 的文件夹叫常规包(regular package)。这个文件可以完全是空的,它的存在标记了"这是一个包",也给了你一个放包级初始化代码的地方。

🔍 延伸:自 Python 3.3(PEP 420)起,不含 __init__.py 的文件夹也能被导入,叫命名空间包(namespace package)——它的设计目的是让同一个包名的内容分散在多个目录下(比如插件系统)。日常项目仍然建议老老实实写 __init__.py:显式、行为可预期,也避免因为忘记建文件而意外得到一个命名空间包。

__init__.py 里可以做导入,简化外部使用:

python
# utils/__init__.py
from .text import clean
from .files import read_json

这样外部就能写 from utils import clean 而不用关心它在哪个子模块。

相对导入(包内部使用):

python
from .text import clean       # 同级模块
from ..config import SETTINGS # 上一级

⚠️ 相对导入只能在包内部用,直接运行含相对导入的文件会报 ImportError: attempted relative import with no known parent package

13.4 if __name__ == "__main__"

python
# tools.py

def main():
    print("运行主程序")

if __name__ == "__main__":
    main()

__name__ 是每个模块都有的内置变量:

  • 直接运行这个文件时,__name__"__main__"
  • 被 import 时,__name__ 是模块名 "tools"

所以这个判断的作用是:这段代码只在直接运行时执行,被导入时不执行

没有它的话,import tools 会意外触发主程序逻辑。这是 Python 项目的标准写法,几乎每个可执行脚本都有。

13.5 模块搜索路径

简化地说,Python 按以下顺序找模块:

  1. 内置模块(sysbuiltins 这些编译进解释器的,最优先,无法被覆盖)
  2. 当前脚本所在目录(交互式解释器下是当前工作目录)
  3. PYTHONPATH 环境变量指定的目录
  4. 标准库目录
  5. 第三方包目录(site-packages
python
import sys
print(sys.path)         # 查看实际生效的搜索路径(这才是准的)

(实际规则还涉及 sys.meta_pathsite 模块对路径的改写等细节,完整说明见官方文档的 The import system。日常记住"以 sys.path 为准,脚本自己的目录排在标准库前面"就够了。)

⚠️ 超高频坑:给自己的文件起了和标准库/第三方库一样的名字。

# 你的目录里有 random.py
import random           # 导入的是你的文件,不是标准库!
random.randint(1, 10)   # AttributeError

不要把文件命名为 random.pyjson.pyemail.pytest.pystring.pytypes.py 等。

13.6 标准库的组织

Python "自带电池"(batteries included)——标准库覆盖了绝大部分常见需求,不用装任何东西:

python
import os, sys, math, random, json, re, time, datetime
import pathlib, collections, itertools, functools
import csv, sqlite3, urllib, http, socket
import unittest, logging, argparse, subprocess
import typing, dataclasses, enum, abc
import asyncio, threading, multiprocessing

第 22 章会详细介绍最常用的那些。

13.7 项目结构示例

一个中小型项目的典型布局:

myproject/
├── pyproject.toml          # 项目配置与依赖声明
├── README.md
├── .gitignore
├── src/
│   └── myproject/
│       ├── __init__.py
│       ├── main.py         # 入口
│       ├── config.py       # 配置
│       ├── models.py       # 数据结构
│       ├── services/       # 业务逻辑
│       │   ├── __init__.py
│       │   └── user.py
│       └── utils/          # 通用工具
│           ├── __init__.py
│           └── text.py
└── tests/
    ├── test_models.py
    └── test_services.py

小脚本不需要这么复杂——单个 .py 文件就好。结构要匹配项目规模。

13.8 本章练习

练习 13.1 创建两个文件:calculator.py 定义加减乘除四个函数,main.py 导入并使用它们。给 calculator.py 加上 if __name__ == "__main__" 的自测代码。

练习 13.2 解释这三种导入的区别,各在什么场景合适:

python
import datetime
from datetime import datetime
from datetime import *

练习 13.3 你写了个文件叫 json.py,里面 import json 报错了。为什么?

<details> <summary>参考答案</summary>

13.1

python
# calculator.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为 0")
    return a / b

if __name__ == "__main__":
    print("自测:")
    print(add(2, 3))        # 5
    print(divide(10, 2))    # 5.0
python
# main.py
from calculator import add, divide

print(add(1, 2))
print(divide(10, 4))

13.2

  • import datetime —— 导入模块,用 datetime.datetime.now()。啰嗦但最不容易混淆。
  • from datetime import datetime —— 导入模块里的 datetime ,用 datetime.now()。最常见的写法。注意这里模块和类同名,容易懵。
  • from datetime import * —— 把所有名字倒进当前命名空间,会污染,不要用。

13.3 因为模块搜索路径的第一项是当前脚本所在目录,import json 找到的是你自己的 json.py(自己 import 自己),而不是标准库的 json。改名即可。 </details>


14. 异常处理

程序总会遇到意外:文件不存在、网络断了、用户输入了乱七八糟的东西。异常机制让你能优雅地应对,而不是让程序崩溃。

14.1 异常长什么样

python
>>> 1 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

读懂 traceback

Traceback (most recent call last):
  File "main.py", line 10, in <module>
    result = process(data)
  File "main.py", line 5, in process
    return int(data["value"])
KeyError: 'value'
  • 从上往下是调用链,最后一行是真正出错的地方
  • 最后一行 KeyError: 'value' 是异常类型和信息

看 traceback 时:先看最后一行(什么错),再看倒数第二个 File(哪一行你的代码触发的)。

14.2 try / except

python
try:
    result = 10 / 0
except ZeroDivisionError:
    print("不能除以零")

捕获多种异常

python
try:
    value = int(input("数字:"))
    result = 100 / value
except ValueError:
    print("请输入数字")
except ZeroDivisionError:
    print("不能是 0")
except (TypeError, KeyError):        # 一次捕获多个
    print("类型或键错误")

拿到异常对象

python
try:
    int("abc")
except ValueError as e:
    print(f"出错了:{e}")            # invalid literal for int()...
    print(type(e).__name__)          # ValueError

14.3 else 和 finally

python
try:
    f = open("data.txt")
except FileNotFoundError:
    print("文件不存在")
else:
    print("没出错时执行")            # try 成功才执行
    data = f.read()
finally:
    print("无论如何都执行")           # 清理工作放这里

执行顺序:

  • 没异常:tryelsefinally
  • 有异常且被捕获:try(到出错处) → exceptfinally
  • 有异常没被捕获:tryfinally → 异常继续向上抛

finally 常用于释放资源,但用 with 语句(14.7 节)通常更好。

💡 else 的价值:把"可能出错的代码"和"成功后的代码"分开,让 try 块尽可能小。

14.4 常见内置异常

异常什么时候发生
ValueError类型对但值不合法,如 int("abc")
TypeError类型不对,如 "a" + 1
KeyError字典里没这个键
IndexError索引越界
AttributeError对象没有这个属性/方法
FileNotFoundError文件不存在
PermissionError没有权限
ZeroDivisionError除以零
ImportError / ModuleNotFoundError导入失败
NameError变量没定义(通常是拼写错误)
StopIteration迭代器耗尽
KeyboardInterrupt用户按了 Ctrl-C

继承关系(部分):

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 └── Exception          ← 你自己的异常应该继承这个
      ├── ValueError
      ├── TypeError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    ├── FileNotFoundError
      │    └── PermissionError
      └── ArithmeticError
           └── ZeroDivisionError

捕获父类会同时捕获所有子类:

python
except LookupError:      # 同时捕获 KeyError 和 IndexError
except OSError:          # 捕获所有文件/系统相关错误

14.5 不要裸捕获

python
# ❌ 最糟糕的写法
try:
    do_something()
except:
    pass

问题:

  1. except:KeyboardInterrupt 都捕获,用户按 Ctrl-C 都退不出去
  2. pass 吞掉所有错误,出了问题完全不知道
  3. 拼写错误、逻辑 bug 全被藏起来

正确的层次

python
# ✅ 最好:捕获具体异常
except ValueError:
    ...

# ✅ 可接受:捕获 Exception 并记录
except Exception as e:
    logger.exception("处理失败")
    raise                  # 记录后重新抛出

# ⚠️ 只在程序最外层用(防止崩溃)
except Exception:
    logger.exception("未预期的错误")

原则:只捕获你知道怎么处理的异常。不知道怎么处理就让它抛上去。

14.6 主动抛异常

python
def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"年龄必须是整数,得到 {type(age).__name__}")
    if age < 0:
        raise ValueError(f"年龄不能为负数:{age}")
    ...

💡 异常信息要包含出错的具体值raise ValueError("参数错误") 几乎没用;raise ValueError(f"端口号必须在 1-65535 之间,得到 {port}") 才能帮上忙。

重新抛出

python
try:
    risky()
except ValueError:
    logger.error("记录一下")
    raise                       # 保留原始 traceback,直接写 raise

异常链(保留原因):

python
try:
    data = json.loads(text)
except json.JSONDecodeError as e:
    raise ConfigError("配置文件格式错误") from e
    # traceback 会显示 "The above exception was the direct cause of..."

自定义异常

python
class AppError(Exception):
    """本应用所有异常的基类。"""

class ConfigError(AppError):
    """配置相关错误。"""

class ValidationError(AppError):
    """数据校验失败。"""
    def __init__(self, field, message):
        self.field = field
        super().__init__(f"{field}: {message}")


try:
    ...
except AppError as e:           # 一次捕获本应用所有异常
    ...

定义一个应用级基类是好习惯——调用方可以选择捕获全部或某一类。

14.7 with 语句与上下文管理器

python
# ❌ 忘记关文件,或者中途异常导致没关
f = open("data.txt")
data = f.read()
f.close()

# ✅ with 保证退出时一定关闭,哪怕出异常
with open("data.txt", encoding="utf-8") as f:
    data = f.read()

with 适用于所有需要"用完必须收尾"的资源:文件、数据库连接、网络连接、锁。

python
with open("a.txt") as fa, open("b.txt", "w") as fb:    # 同时管理多个
    fb.write(fa.read())

自定义上下文管理器——最简单的方式是用 contextlib

python
from contextlib import contextmanager
import time

@contextmanager
def timer(name):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{name} 耗时 {elapsed:.3f} 秒")


with timer("数据处理"):
    do_heavy_work()
# 数据处理 耗时 1.234 秒

yield 之前是"进入时做的事",之后是"退出时做的事"。用 try/finally 包住 yield 才能保证异常时也执行清理。

也可以写成类(第 18 章会讲得更清楚):

python
class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        print(f"耗时 {time.perf_counter() - self.start:.3f} 秒")
        return False        # 返回 True 会吞掉异常,一般返回 False

14.8 EAFP vs LBYL

Python 社区推崇 EAFP(Easier to Ask Forgiveness than Permission,先做再说,出错再处理),而不是 LBYL(Look Before You Leap,先检查再做)。

python
# LBYL —— 其他语言的常见风格
if os.path.exists(path):
    with open(path) as f:       # ⚠️ 检查和打开之间文件可能被删(竞态条件)
        ...

# EAFP —— Python 风格
try:
    with open(path) as f:
        ...
except FileNotFoundError:
    ...
python
# LBYL
if "key" in d:
    value = d["key"]
else:
    value = default

# EAFP
try:
    value = d["key"]
except KeyError:
    value = default

# 最好:用现成的
value = d.get("key", default)

EAFP 的好处:没有竞态条件、少一次检查开销(异常不发生时几乎零成本)。

14.9 断言 assert

python
def divide(a, b):
    assert b != 0, "除数不能为 0"
    return a / b

assert 条件, 消息 等价于 if not 条件: raise AssertionError(消息)

⚠️ 重要:用 python -O 运行时所有 assert 会被完全移除。所以:

  • ✅ assert 用于开发期的内部一致性检查("这里绝不该发生")
  • 绝不能用 assert 做用户输入校验、权限检查
python
assert user.is_admin        # ❌ 生产环境可能被跳过 = 安全漏洞
if not user.is_admin:       # ✅
    raise PermissionError()

14.10 本章练习

练习 14.1 写一个函数 safe_int(s, default=0),尝试把字符串转成整数,失败返回默认值。

练习 14.2 改写这段代码,用 with 语句:

python
f = open("data.txt")
data = f.read()
f.close()

练习 14.3 下面代码有什么问题?

python
try:
    result = compute()
except:
    pass

练习 14.4 写一个函数,读取一个 JSON 配置文件,处理三种错误情况:文件不存在、JSON 格式错误、缺少必需的键。每种给出清晰的错误提示。

练习 14.5 实现一个上下文管理器 suppress_errors(),让 with 块里的异常被忽略并打印警告。

练习 14.6 定义一个异常体系:ShopError 作为基类,下面有 OutOfStockError(带商品名)和 PaymentError(带金额)。

<details> <summary>参考答案</summary>

14.1

python
def safe_int(s, default=0):
    try:
        return int(s)
    except (ValueError, TypeError):
        return default

safe_int("42")     # 42
safe_int("abc")    # 0
safe_int(None)     # 0
safe_int("x", -1)  # -1

14.2

python
with open("data.txt", encoding="utf-8") as f:
    data = f.read()

14.3 三个问题:

  1. except: 会捕获 KeyboardInterruptSystemExit,用户无法 Ctrl-C 退出
  2. pass 静默吞掉错误,出问题无法排查
  3. 没有说明捕获的是什么异常、为什么能忽略

14.4

python
import json
from pathlib import Path

class ConfigError(Exception):
    pass

def load_config(path, required_keys=("host", "port")):
    p = Path(path)
    try:
        text = p.read_text(encoding="utf-8")
    except FileNotFoundError:
        raise ConfigError(f"配置文件不存在:{p.absolute()}") from None

    try:
        config = json.loads(text)
    except json.JSONDecodeError as e:
        raise ConfigError(f"配置文件不是合法 JSON(第 {e.lineno} 行):{e.msg}") from e

    missing = [k for k in required_keys if k not in config]
    if missing:
        raise ConfigError(f"配置缺少必需项:{', '.join(missing)}")

    return config

14.5

python
from contextlib import contextmanager

@contextmanager
def suppress_errors():
    try:
        yield
    except Exception as e:
        print(f"⚠️ 已忽略错误:{type(e).__name__}: {e}")

with suppress_errors():
    1 / 0
print("继续执行")

标准库其实已经有了:from contextlib import suppress; with suppress(ZeroDivisionError): ...

14.6

python
class ShopError(Exception):
    """商店相关异常基类。"""

class OutOfStockError(ShopError):
    def __init__(self, product):
        self.product = product
        super().__init__(f"商品缺货:{product}")

class PaymentError(ShopError):
    def __init__(self, amount, reason="未知原因"):
        self.amount = amount
        super().__init__(f"支付 {amount} 元失败:{reason}")

</details>


15. 文件与路径

15.1 pathlib:现代路径操作

老代码用 os.path,新代码应该用 pathlib——面向对象、跨平台、可读性好得多。

python
from pathlib import Path

p = Path("data/report.txt")

# 拼接路径用 / 运算符
base = Path("/home/user")
full = base / "documents" / "file.txt"      # /home/user/documents/file.txt

# 常用属性
p.name        # 'report.txt'   文件名
p.stem        # 'report'       不带扩展名
p.suffix      # '.txt'         扩展名
p.parent      # Path('data')   父目录
p.parts       # ('data', 'report.txt')
p.absolute()  # 绝对路径

# 判断
p.exists()      # 是否存在
p.is_file()     # 是文件
p.is_dir()      # 是目录

# 特殊位置
Path.cwd()      # 当前工作目录
Path.home()     # 用户主目录
Path(__file__).parent    # 当前脚本所在目录(写脚本时超常用)

目录操作

python
d = Path("output")
d.mkdir()                                # 创建目录,已存在会报错
d.mkdir(parents=True, exist_ok=True)     # 递归创建,已存在不报错 ← 常用

# 遍历
for f in d.iterdir():              # 直接子项
    print(f)

for f in d.glob("*.txt"):          # 匹配当前层
    print(f)

for f in d.rglob("*.py"):          # 递归匹配所有层
    print(f)

# 只要文件
files = [f for f in d.rglob("*") if f.is_file()]

重命名、删除、复制

python
p.rename("newname.txt")
p.unlink()                    # 删除文件
p.unlink(missing_ok=True)     # 不存在也不报错
d.rmdir()                     # 删除空目录

import shutil
shutil.copy("a.txt", "b.txt")       # 复制文件
shutil.copytree("src", "dst")       # 复制整个目录树
shutil.rmtree("dir")                # 删除目录及内容 ⚠️ 危险,不可恢复
shutil.move("a", "b")               # 移动

快捷读写(小文件用这个,一行搞定):

python
text = Path("a.txt").read_text(encoding="utf-8")
Path("b.txt").write_text("内容", encoding="utf-8")

data = Path("img.png").read_bytes()
Path("copy.png").write_bytes(data)

15.2 open() 与文件模式

python
with open("file.txt", mode="r", encoding="utf-8") as f:
    ...
模式含义
"r"读(默认),文件不存在报错
"w"写,清空已有内容,不存在则创建
"a"追加,写到末尾
"x"独占创建,文件已存在则报错
"b"二进制模式,和上面组合用,如 "rb""wb"
"+"读写,如 "r+"

⚠️ `"w"` 会清空文件。想追加内容一定要用 "a"

⚠️ 文本模式一定要指定 `encoding="utf-8"`。不指定的话用系统默认编码,同一份代码在不同机器上行为不同——这是跨平台 bug 的经典来源。

15.3 读文件

python
with open("data.txt", encoding="utf-8") as f:
    content = f.read()          # 一次读全部,返回一个字符串

with open("data.txt", encoding="utf-8") as f:
    lines = f.readlines()       # 返回列表,每行一个元素(保留换行符)

with open("data.txt", encoding="utf-8") as f:
    for line in f:              # ✅ 逐行迭代,内存友好,大文件必用
        print(line.rstrip())    # rstrip 去掉行尾换行符

💡 处理大文件(几百 MB 以上)必须用逐行迭代,read() 会把整个文件装进内存。

15.4 写文件

python
with open("out.txt", "w", encoding="utf-8") as f:
    f.write("第一行\n")           # write 不会自动加换行符
    f.write("第二行\n")
    f.writelines(["a\n", "b\n"])  # 也不会自动加

# 用 print 写文件更方便(自动换行)
with open("out.txt", "w", encoding="utf-8") as f:
    print("第一行", file=f)
    print("第二行", file=f)

15.5 常见文件格式

JSON——配置文件、API 数据交换的事实标准

python
import json

# 写
data = {"name": "张三", "tags": ["a", "b"], "age": 25}
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
    # ensure_ascii=False → 中文正常显示而不是 \uXXXX
    # indent=2 → 缩进美化

# 读
with open("data.json", encoding="utf-8") as f:
    data = json.load(f)

# 字符串版本
s = json.dumps(data, ensure_ascii=False)
data = json.loads(s)

⚠️ JSON 只支持有限的类型(字符串、数字、布尔、null、数组、对象)。datetimeset、自定义对象需要转换:

python
json.dumps(obj, default=str)             # 简单粗暴:不认识的都转字符串
json.dumps({"d": date.today().isoformat()})   # 手动转

CSV——表格数据

python
import csv

# 写
rows = [{"name": "张三", "age": 25}, {"name": "李四", "age": 30}]
with open("out.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "age"])
    writer.writeheader()
    writer.writerows(rows)

# 读
with open("out.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["name"], row["age"])    # row 是字典

⚠️ 打开 CSV 一定要加 newline="",否则 Windows 上会多出空行。

💡 Excel 打开 UTF-8 的 CSV 可能乱码,用 encoding="utf-8-sig"(带 BOM)能解决。

其他格式

python
# 二进制序列化 Python 对象(只在信任的场景用!pickle 可执行任意代码)
import pickle
pickle.dump(obj, open("f.pkl", "wb"))

# TOML(读取,Python 3.11+ 内置)
import tomllib
with open("pyproject.toml", "rb") as f:      # 注意是 "rb"
    config = tomllib.load(f)

# YAML 需要第三方库
# pip install pyyaml
import yaml
config = yaml.safe_load(open("config.yaml", encoding="utf-8"))

15.6 实用场景

批量重命名

python
from pathlib import Path

for i, f in enumerate(sorted(Path("photos").glob("*.jpg")), start=1):
    f.rename(f.parent / f"photo_{i:03d}.jpg")

统计目录大小

python
total = sum(f.stat().st_size for f in Path(".").rglob("*") if f.is_file())
print(f"{total / 1024 / 1024:.2f} MB")

查找并处理所有 Python 文件

python
for py in Path("src").rglob("*.py"):
    text = py.read_text(encoding="utf-8")
    if "TODO" in text:
        print(f"{py}: 有待办事项")

安全地写文件(先写临时文件再替换,避免写一半崩溃损坏原文件)

python
from pathlib import Path

target = Path("important.json")
tmp = target.with_suffix(".json.tmp")
tmp.write_text(json.dumps(data), encoding="utf-8")
tmp.replace(target)      # 原子操作

15.7 本章练习

练习 15.1 写一个程序,统计一个文本文件有多少行、多少个单词、多少个字符。

练习 15.2 写一个程序,读取一个文件,把所有行倒序写入另一个文件。

练习 15.3pathlib 找出当前目录(含子目录)下所有大于 1MB 的文件,按大小排序输出。

练习 15.4 写一个程序,把一个 JSON 文件里的联系人列表导出成 CSV。

练习 15.5 为什么下面的代码在某些机器上读中文会乱码?怎么修?

python
with open("data.txt") as f:
    print(f.read())

练习 15.6 写一个函数,安全地读取 JSON 文件:文件不存在或格式错误时返回一个默认字典,而不是崩溃。

<details> <summary>参考答案</summary>

15.1

python
from pathlib import Path

text = Path("data.txt").read_text(encoding="utf-8")
lines = text.splitlines()

print(f"行数:{len(lines)}")
print(f"单词数:{len(text.split())}")
print(f"字符数:{len(text)}")

15.2

python
from pathlib import Path

lines = Path("in.txt").read_text(encoding="utf-8").splitlines()
Path("out.txt").write_text("\n".join(reversed(lines)), encoding="utf-8")

15.3

python
from pathlib import Path

MB = 1024 * 1024
big = [f for f in Path(".").rglob("*") if f.is_file() and f.stat().st_size > MB]

for f in sorted(big, key=lambda p: p.stat().st_size, reverse=True):
    print(f"{f.stat().st_size / MB:8.2f} MB  {f}")

15.4

python
import csv, json
from pathlib import Path

contacts = json.loads(Path("contacts.json").read_text(encoding="utf-8"))

with open("contacts.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=contacts[0].keys())
    writer.writeheader()
    writer.writerows(contacts)

15.5 没指定 encoding,Python 会用系统默认编码(中文 Windows 上常是 GBK/cp936),读 UTF-8 文件就会乱码或抛 UnicodeDecodeError

python
with open("data.txt", encoding="utf-8") as f:
    print(f.read())

15.6

python
import json
from pathlib import Path

def load_json(path, default=None):
    if default is None:
        default = {}
    try:
        return json.loads(Path(path).read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError) as e:
        print(f"⚠️ 读取 {path} 失败({type(e).__name__}),使用默认值")
        return default

</details>


第 5 部分 · 面向对象

到这里你已经能写出有用的程序了。面向对象(OOP)不会让你的程序"能做更多事",但它能让复杂的程序更容易理解和修改

16. 类与对象

16.1 为什么需要类

假设要管理一堆学生。用字典也行:

python
student = {"name": "张三", "scores": [85, 92, 78]}

def average(s):
    return sum(s["scores"]) / len(s["scores"])

但问题会逐渐显现:

  • 打错键名(s["score"])要到运行时才发现
  • 数据和操作它的函数分散在各处
  • 没法保证"每个学生都有 scores 字段"

类把数据操作数据的方法捆在一起:

python
class Student:
    def __init__(self, name):
        self.name = name
        self.scores = []

    def add_score(self, score):
        self.scores.append(score)

    def average(self):
        if not self.scores:
            return 0
        return sum(self.scores) / len(self.scores)


s = Student("张三")
s.add_score(85)
s.add_score(92)
print(s.average())      # 88.5

16.2 类的基本结构

python
class Dog:
    species = "犬科"                 # 类属性:所有实例共享

    def __init__(self, name, age):   # 构造方法
        self.name = name             # 实例属性:每个实例独有
        self.age = age

    def bark(self):                  # 实例方法
        return f"{self.name} 汪汪叫"

    def birthday(self):
        self.age += 1
        return self.age


d = Dog("旺财", 3)      # 创建实例,自动调用 __init__
d.name                  # '旺财'
d.bark()                # '旺财 汪汪叫'
d.species               # '犬科'
Dog.species             # '犬科'  也可以通过类访问

关键概念

  • (class)是模板,实例(instance)/对象(object)是按模板造出来的具体东西
  • __init__ 在创建实例时自动调用,用来初始化属性
  • `self` 指向实例本身,是每个实例方法的第一个参数,调用时不用传(Python 自动传)
python
d.bark()          # Python 实际执行的是 Dog.bark(d)

⚠️ 忘写 self 是新手最常见的错误:

python
class Dog:
    def bark():                 # ❌ TypeError: bark() takes 0 positional arguments but 1 was given
        return "汪"

16.3 类属性 vs 实例属性

python
class Counter:
    total = 0                  # 类属性,所有实例共享

    def __init__(self):
        Counter.total += 1
        self.id = Counter.total   # 实例属性,各自独立

a = Counter()
b = Counter()
print(Counter.total)     # 2
print(a.id, b.id)        # 1 2

⚠️ 坑:可变类属性被共享

python
class Basket:
    items = []              # ❌ 所有实例共用一个列表!

    def add(self, x):
        self.items.append(x)

a, b = Basket(), Basket()
a.add("苹果")
print(b.items)           # ['苹果']  ← b 也受影响了

正确做法——可变数据放 __init__ 里:

python
class Basket:
    def __init__(self):
        self.items = []      # ✅ 每个实例一个新列表

这和第 11 章"可变默认参数"是同一类问题。

16.4 方法的三种类型

python
class Circle:
    PI = 3.14159

    def __init__(self, radius):
        self.radius = radius

    def area(self):                       # 实例方法:操作某个实例
        return self.PI * self.radius ** 2

    @classmethod
    def from_diameter(cls, d):            # 类方法:第一个参数是类本身
        return cls(d / 2)                 # 常用于"另一种构造方式"

    @staticmethod
    def is_valid_radius(r):               # 静态方法:跟类和实例都无关
        return r > 0                      # 只是逻辑上属于这个类


c1 = Circle(5)
c2 = Circle.from_diameter(10)             # 等价于 Circle(5)
Circle.is_valid_radius(-1)                # False
类型第一个参数什么时候用
实例方法self需要访问/修改实例数据(绝大多数情况)
类方法cls替代构造函数、操作类属性
静态方法相关的工具函数,不需要 self/cls

16.5 封装:公开、保护、私有

Python 没有真正的访问控制,靠命名约定

python
class Account:
    def __init__(self, balance):
        self.owner = "张三"          # 公开:随便用
        self._balance = balance      # 单下划线:内部使用,请勿直接碰(约定)
        self.__pin = "1234"          # 双下划线:名称改写,更强的"别碰"

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("金额必须为正")
        self._balance += amount
  • _name —— "这是内部实现细节,我随时可能改,你别依赖它"。仅仅是约定,Python 不阻止你访问。
  • __name —— 触发名称改写(name mangling),实际存成 _ClassName__name。主要目的是避免子类意外覆盖,不是安全机制。
python
a = Account(100)
a._balance           # 能访问,但你不该这么做
a.__pin              # AttributeError
a._Account__pin      # '1234'  —— 还是能拿到,所以不是"安全"

💡 Python 的哲学是"我们都是成年人"——用约定而不是强制。

16.6 property:把方法伪装成属性

python
class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("低于绝对零度")
        self._celsius = value

    @property
    def fahrenheit(self):            # 只读的计算属性
        return self._celsius * 9 / 5 + 32


t = Temperature(25)
t.celsius          # 25      像属性一样访问,实际调用了方法
t.celsius = 30     # 触发 setter,会做校验
t.fahrenheit       # 86.0    计算得出
t.fahrenheit = 100 # AttributeError: 没有 setter
t.celsius = -300   # ValueError

property 的价值:可以先用普通属性,将来需要加校验/计算时改成 property,调用方代码完全不用改。这是 Python 不需要处处写 getter/setter 的原因。

python
# ❌ Java 风格,Python 里没必要
class Person:
    def get_name(self): return self._name
    def set_name(self, v): self._name = v

# ✅ Python 风格:先用普通属性
class Person:
    def __init__(self, name):
        self.name = name

16.7 本章练习

练习 16.1 定义一个 Rectangle 类,有 widthheight 属性,提供 area()perimeter() 方法,以及一个 is_square 属性(property)。

练习 16.2 下面代码有什么问题?

python
class Student:
    grades = []
    def add_grade(self, g):
        self.grades.append(g)

练习 16.3 定义一个 BankAccount 类:

  • 余额不能直接修改,只能通过 depositwithdraw
  • 取款超过余额抛异常
  • 记录所有交易历史
  • 提供一个类方法 from_dict(data) 从字典创建账户

练习 16.4 定义一个 Stack(栈)类,支持 pushpoppeekis_emptysize

练习 16.5Circle 类加一个 radius property,setter 里校验半径必须为正。

<details> <summary>参考答案</summary>

16.1

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    @property
    def is_square(self):
        return self.width == self.height

16.2 grades 是类属性,所有学生共享同一个列表。

python
class Student:
    def __init__(self):
        self.grades = []
    def add_grade(self, g):
        self.grades.append(g)

16.3

python
class InsufficientFunds(Exception):
    pass

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance
        self._history = []

    @property
    def balance(self):
        return self._balance

    @property
    def history(self):
        return tuple(self._history)      # 返回不可变副本,防止外部篡改

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError(f"存款金额必须为正,得到 {amount}")
        self._balance += amount
        self._history.append(("deposit", amount))

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError(f"取款金额必须为正,得到 {amount}")
        if amount > self._balance:
            raise InsufficientFunds(f"余额 {self._balance} 不足以取出 {amount}")
        self._balance -= amount
        self._history.append(("withdraw", amount))

    @classmethod
    def from_dict(cls, data):
        return cls(data["owner"], data.get("balance", 0))

16.4

python
class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("空栈不能 pop")
        return self._items.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError("空栈不能 peek")
        return self._items[-1]

    def is_empty(self):
        return len(self._items) == 0

    def size(self):
        return len(self._items)

    def __len__(self):              # 让 len(stack) 也能用
        return len(self._items)

    def __repr__(self):
        return f"Stack({self._items})"

16.5

python
class Circle:
    def __init__(self, radius):
        self.radius = radius        # 这里就会触发 setter 的校验

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError(f"半径必须为正数,得到 {value}")
        self._radius = value

</details>


17. 继承与多态

17.1 继承:复用与特化

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

    def describe(self):
        return f"{self.name} 说:{self.speak()}"


class Dog(Animal):              # Dog 继承 Animal
    def speak(self):            # 重写父类方法
        return "汪汪"


class Cat(Animal):
    def speak(self):
        return "喵喵"


Dog("旺财").describe()      # '旺财 说:汪汪'
Cat("咪咪").describe()      # '咪咪 说:喵喵'

子类自动获得父类的所有属性和方法,可以:

  • 直接用describe 没重写,直接继承)
  • 重写speak 各自实现)
  • 扩展(新增父类没有的方法)

17.2 super():调用父类实现

python
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age


class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)     # 先让父类初始化
        self.breed = breed              # 再加自己的

super() 也能用在普通方法里,实现"在父类基础上加东西":

python
class LoggedList(list):
    def append(self, item):
        print(f"添加:{item}")
        super().append(item)            # 调用 list 原本的 append

💡 重写 `__init__` 时几乎总是要调用 `super().__init__()`,否则父类的初始化逻辑不会执行。

17.3 多态:同样的调用,不同的行为

python
animals = [Dog("旺财"), Cat("咪咪"), Animal("小明")]

for a in animals:
    print(a.speak())        # 每个对象按自己的类型响应

调用方不需要知道具体是什么类,只需要知道"它有 speak 方法"。这就是多态,也是面向对象最实用的地方——新增一种动物不需要改任何已有代码

17.4 鸭子类型

Python 的多态不要求继承关系。"如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子":

python
class Duck:
    def speak(self): return "嘎嘎"

class Robot:                       # 跟 Animal 毫无关系
    def speak(self): return "滴滴"

for x in [Dog("旺财"), Robot()]:
    print(x.speak())               # 都能工作

这带来了极大的灵活性。代价是错误在运行时才暴露——所以类型注解(第 21 章)在大项目里很有价值。

17.5 isinstance 与 issubclass

python
isinstance(d, Dog)          # True
isinstance(d, Animal)       # True   —— 子类实例也是父类实例
isinstance(d, (Dog, Cat))   # True   —— 可以传元组

issubclass(Dog, Animal)     # True
type(d) is Dog              # True   —— 精确类型判断,不考虑继承

💡 用 isinstance 而不是 type(x) == Dog——前者尊重继承关系。

⚠️ 但满屏的 isinstance 判断通常是设计有问题的信号:

python
# ❌ 每加一种动物都要改这个函数
def make_sound(animal):
    if isinstance(animal, Dog):
        return "汪汪"
    elif isinstance(animal, Cat):
        return "喵喵"

# ✅ 多态:加新动物不用改这里
def make_sound(animal):
    return animal.speak()

17.6 抽象基类

想强制子类必须实现某些方法:

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    @abstractmethod
    def perimeter(self):
        ...

    def describe(self):                  # 可以有普通方法
        return f"面积 {self.area()},周长 {self.perimeter()}"


class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h
    def area(self):
        return self.w * self.h
    def perimeter(self):
        return 2 * (self.w + self.h)


Shape()          # TypeError: Can't instantiate abstract class Shape
Rectangle(3, 4).describe()   # '面积 12,周长 14'

抽象基类的作用:明确契约。它告诉别人"要做我的子类,必须实现这几个方法",而且忘记实现会在创建实例时立刻报错,不用等到调用时。

17.7 多重继承与 MRO

Python 允许一个类继承多个父类:

python
class Serializable:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class Comparable:
    def __lt__(self, other):
        return self.value < other.value

class Item(Serializable, Comparable):
    def __init__(self, value):
        self.value = value

MRO(Method Resolution Order)决定了查找方法的顺序:

python
Item.__mro__
# (Item, Serializable, Comparable, object)

Python 用 C3 线性化算法计算 MRO,规则大致是"从左到右,深度优先,但子类永远在父类前面"。

⚠️ 多重继承容易造成复杂难解的问题(菱形继承、初始化顺序)。实践建议

  • 优先用组合而不是继承
  • 多重继承只用于 mixin(提供单一附加能力的小类,不含状态)
python
# ❌ 继承:Car "是一个" Engine?不对
class Car(Engine): ...

# ✅ 组合:Car "有一个" Engine
class Car:
    def __init__(self):
        self.engine = Engine()

17.8 何时该用继承

用继承的信号:

  • 存在真实的"是一个"关系(Dog 是一个 Animal
  • 子类能完全替代父类使用(里氏替换原则)
  • 有大量共享行为

不该用继承的信号:

  • 只是想复用几行代码(用函数或组合)
  • 继承层次超过 3 层(几乎总是设计问题)
  • 子类要重写父类的大部分方法(说明抽象错了)

Python 特别提示:不要继承 listdict 这些内置类型来"加功能",很多内置方法不会走你重写的逻辑。用组合,或者继承 collections.UserList / UserDict

17.9 本章练习

练习 17.1 定义一个 Employee 基类(有 namebase_salarycalculate_pay() 方法),派生出 Manager(工资 = 基本工资 × 1.5 + 奖金)和 Intern(工资 = 基本工资 × 0.6)。

练习 17.2 用抽象基类定义一个 Storage 接口,要求实现 save(key, value)load(key)。写两个实现:MemoryStorage(存字典)和 FileStorage(存 JSON 文件)。

练习 17.3 下面代码为什么 dog.name 报错?

python
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, breed):
        self.breed = breed

dog = Dog("柴犬")
print(dog.name)

练习 17.4 解释组合和继承的区别,并给一个应该用组合而不是继承的例子。

<details> <summary>参考答案</summary>

17.1

python
class Employee:
    def __init__(self, name, base_salary):
        self.name = name
        self.base_salary = base_salary

    def calculate_pay(self):
        return self.base_salary

    def __repr__(self):
        return f"{type(self).__name__}({self.name}, {self.calculate_pay():.0f})"


class Manager(Employee):
    def __init__(self, name, base_salary, bonus=0):
        super().__init__(name, base_salary)
        self.bonus = bonus

    def calculate_pay(self):
        return self.base_salary * 1.5 + self.bonus


class Intern(Employee):
    def calculate_pay(self):
        return self.base_salary * 0.6


staff = [Manager("张三", 20000, 5000), Intern("李四", 10000), Employee("王五", 15000)]
for e in staff:
    print(e)

17.2

python
import json
from abc import ABC, abstractmethod
from pathlib import Path

class Storage(ABC):
    @abstractmethod
    def save(self, key, value): ...

    @abstractmethod
    def load(self, key): ...


class MemoryStorage(Storage):
    def __init__(self):
        self._data = {}
    def save(self, key, value):
        self._data[key] = value
    def load(self, key):
        return self._data.get(key)


class FileStorage(Storage):
    def __init__(self, path):
        self.path = Path(path)
        if not self.path.exists():
            self.path.write_text("{}", encoding="utf-8")

    def _read(self):
        return json.loads(self.path.read_text(encoding="utf-8"))

    def save(self, key, value):
        data = self._read()
        data[key] = value
        self.path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")

    def load(self, key):
        return self._read().get(key)

17.3 Dog.__init__ 重写了父类的 __init__ 但没调用 super().__init__(),所以 self.name 从来没被设置过。

python
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

17.4

  • 继承表达"是一个"(is-a):Dog 是一个 Animal
  • 组合表达"有一个"(has-a):Car 有一个 Engine

组合更灵活:可以运行时替换、不受继承层次约束、不会因为父类改动而意外破坏子类。

例子:一个 Logger 功能,不应该让所有需要日志的类继承 LoggerBase,而应该:

python
class Service:
    def __init__(self, logger):
        self.logger = logger        # 组合,可以注入不同的 logger

</details>


18. 特殊方法与数据类

18.1 特殊方法(魔术方法)

名字前后带双下划线的方法叫特殊方法(dunder method,double underscore)。它们让你的类能用上 Python 的内置语法。

python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):                      # 开发者看的表示
        return f"Vector({self.x}, {self.y})"

    def __str__(self):                       # 用户看的表示
        return f"({self.x}, {self.y})"

    def __eq__(self, other):                 # ==
        if not isinstance(other, Vector):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):                      # 定义了 __eq__ 就要定义 __hash__
        return hash((self.x, self.y))

    def __add__(self, other):                # +
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):               # *
        return Vector(self.x * scalar, self.y * scalar)

    def __abs__(self):                       # abs()
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __len__(self):                       # len()
        return 2

    def __getitem__(self, i):                # v[0]
        return (self.x, self.y)[i]

    def __bool__(self):                      # if v:
        return bool(self.x or self.y)


v1 = Vector(1, 2)
v2 = Vector(3, 4)

v1 + v2          # Vector(4, 6)
v1 * 3           # Vector(3, 6)
abs(v2)          # 5.0
v1 == Vector(1, 2)   # True
len(v1)          # 2
v1[0]            # 1
print(v1)        # (1, 2)      —— 用 __str__
v1               # Vector(1, 2) —— REPL 里用 __repr__

18.2 常用特殊方法一览

表示

python
__repr__(self)      # repr(x),调试用,应该尽量能还原对象
__str__(self)       # str(x)、print(x),给人看的
__format__(self, spec)   # f"{x:spec}"

💡 只写一个的话写 __repr__——__str__ 没定义时会退化到用 __repr__。好的 __repr__ 应该长得像能重建对象的代码。

比较

python
__eq__  __ne__  __lt__  __le__  __gt__  __ge__

💡 用 functools.total_ordering 装饰器,只需实现 __eq____lt__,其余自动生成。

运算

python
__add__  __sub__  __mul__  __truediv__  __floordiv__  __mod__  __pow__
__radd__ ...     # 反向运算(3 * vector 时调用 vector.__rmul__)
__iadd__ ...     # 原地运算(+=)
__neg__  __abs__

容器

python
__len__(self)               # len(x)
__getitem__(self, key)      # x[key]
__setitem__(self, key, v)   # x[key] = v
__delitem__(self, key)      # del x[key]
__contains__(self, item)    # item in x
__iter__(self)              # for i in x
__next__(self)              # 迭代器协议

其他

python
__call__(self, ...)         # 让实例能像函数一样调用 x()
__enter__ / __exit__        # with 语句
__hash__(self)              # 作为字典键/集合元素
__bool__(self)              # if x:
__getattr__(self, name)     # 访问不存在的属性时兜底
__slots__ = ("x", "y")      # 限制属性,省内存

18.3 dataclass:少写样板代码

写一个纯数据类要重复很多东西:

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

dataclass 一行搞定:

python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1, 2)
p                    # Point(x=1, y=2)      __repr__ 自动生成
p == Point(1, 2)     # True                 __eq__ 自动生成

常用选项

python
from dataclasses import dataclass, field

@dataclass(frozen=True)      # 不可变,且自动生成 __hash__
class Config:
    host: str = "localhost"          # 带默认值
    port: int = 8080
    tags: list[str] = field(default_factory=list)   # ⚠️ 可变默认值必须用 field
    _secret: str = field(default="", repr=False)     # 不出现在 repr 里


@dataclass(order=True)       # 自动生成 < > <= >=,按字段顺序比较
class Version:
    major: int
    minor: int

⚠️ 可变默认值必须用 field(default_factory=list),直接写 tags: list = [] 会报错(dataclass 帮你挡住了第 11 章那个坑)。

后处理

python
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)      # 不作为构造参数

    def __post_init__(self):
        self.area = self.width * self.height

转换工具

python
from dataclasses import asdict, astuple, replace

asdict(p)                  # {'x': 1, 'y': 2}
astuple(p)                 # (1, 2)
replace(p, x=10)           # Point(x=10, y=2)  返回新实例

💡 什么时候用 dataclass:任何"主要用来装数据"的类。它比字典安全(字段名写错会立刻报错),比手写类省事。这应该是你的默认选择

18.4 其他数据容器方案对比

方案可变类型校验适合场景
dict结构不固定、来自 JSON
namedtuple轻量的不可变记录
dataclass可选❌(只是注解)默认选择
NamedTuple(typing)需要元组行为 + 类型注解
pydantic.BaseModel✅ 运行时校验处理外部输入(API、配置)
python
# typing.NamedTuple —— 带注解的不可变记录
from typing import NamedTuple
class Point(NamedTuple):
    x: float
    y: float

# pydantic(第三方,需 pip install pydantic)—— 会真的校验类型并转换
from pydantic import BaseModel
class User(BaseModel):
    name: str
    age: int
User(name="张三", age="25")     # age 自动转成 int 25
User(name="张三", age="abc")    # ValidationError

处理来自外部(HTTP 请求、配置文件、用户输入)的数据,用 pydantic;内部数据结构用 dataclass。

18.5 Enum:枚举

用具名常量代替"魔法字符串":

python
from enum import Enum, auto

class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    CLOSED = "closed"

Status.ACTIVE            # <Status.ACTIVE: 'active'>
Status.ACTIVE.value      # 'active'
Status.ACTIVE.name       # 'ACTIVE'
Status("active")         # <Status.ACTIVE: 'active'>  从值反查

for s in Status:         # 可迭代
    print(s)
python
class Color(Enum):
    RED = auto()         # 自动赋值 1, 2, 3
    GREEN = auto()
    BLUE = auto()

from enum import StrEnum       # Python 3.11+,成员可以直接当字符串用
class Env(StrEnum):
    DEV = "dev"
    PROD = "prod"

Env.DEV == "dev"         # True

好处:拼写错误立刻报错、IDE 能自动补全、能列举所有可能值。

python
# ❌ 魔法字符串
if order.status == "actve":     # 拼错了,静默失效

# ✅ 枚举
if order.status is Status.ACTIVE:   # 拼错会 AttributeError

18.6 本章练习

练习 18.1Vector 类添加 __sub__(减法)和 __rmul__(让 3 * v 也能用)。

练习 18.2 用 dataclass 定义一个 Book 类:title、author、year、tags(列表,默认空)、isbn(不出现在 repr 里)。

练习 18.3 定义一个 Money 类,用 frozen dataclass,支持加法和比较,金额用 Decimal 存储,币种不同时相加抛异常。

练习 18.4 定义一个 Playlist 类,支持 len()playlist[0]for song in playlist"歌名" in playlist

练习 18.5 用 Enum 重构下面代码:

python
def get_discount(user_type):
    if user_type == "vip":
        return 0.8
    elif user_type == "svip":
        return 0.6
    return 1.0

<details> <summary>参考答案</summary>

18.1

python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)

    __rmul__ = __mul__          # 标量乘法可交换,直接复用

18.2

python
from dataclasses import dataclass, field

@dataclass
class Book:
    title: str
    author: str
    year: int
    tags: list[str] = field(default_factory=list)
    isbn: str = field(default="", repr=False)

18.3

python
from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True, order=True)
class Money:
    amount: Decimal
    currency: str = "CNY"

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError(f"币种不匹配:{self.currency} vs {other.currency}")
        return Money(self.amount + other.amount, self.currency)

    def __str__(self):
        return f"{self.amount:.2f} {self.currency}"


a = Money(Decimal("10.50"))
b = Money(Decimal("5.25"))
print(a + b)      # 15.75 CNY

18.4

python
class Playlist:
    def __init__(self, songs=None):
        self._songs = list(songs or [])

    def add(self, song):
        self._songs.append(song)

    def __len__(self):
        return len(self._songs)

    def __getitem__(self, i):
        return self._songs[i]

    def __iter__(self):
        return iter(self._songs)

    def __contains__(self, song):
        return song in self._songs

    def __repr__(self):
        return f"Playlist({len(self)} 首)"

注意:只要实现了 __getitem__,Python 就能自动支持 forin,但显式实现 __iter____contains__ 更高效也更清晰。

18.5

python
from enum import Enum

class UserType(Enum):
    NORMAL = "normal"
    VIP = "vip"
    SVIP = "svip"

DISCOUNTS = {
    UserType.NORMAL: 1.0,
    UserType.VIP: 0.8,
    UserType.SVIP: 0.6,
}

def get_discount(user_type: UserType) -> float:
    return DISCOUNTS[user_type]

</details>


第 6 部分 · 进阶特性

19. 迭代器与生成器

19.1 可迭代对象与迭代器

可迭代对象(iterable):能用 for 遍历的东西——列表、字符串、字典、文件、range……

迭代器(iterator):真正执行"取下一个"动作的对象。

python
lst = [1, 2, 3]
it = iter(lst)         # 从可迭代对象得到迭代器

next(it)               # 1
next(it)               # 2
next(it)               # 3
next(it)               # StopIteration 异常

for 循环背后做的事:

python
# for x in lst: print(x)
# 实际上等价于:
it = iter(lst)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    print(x)

迭代器是一次性的

python
it = iter([1, 2, 3])
list(it)     # [1, 2, 3]
list(it)     # []  ← 已经耗尽了

⚠️ 这个特性会咬人:

python
z = zip([1,2], "ab")
list(z)      # [(1, 'a'), (2, 'b')]
list(z)      # []  ← zip 返回的是迭代器!

mapfilterzipenumeratereversed 在 Python 3 里都返回迭代器,需要反复使用就先 list() 一下。

19.2 自定义迭代器

python
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return CountdownIterator(self.start)


class CountdownIterator:
    def __init__(self, current):
        self.current = current

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1


for n in Countdown(3):
    print(n)      # 3 2 1

写起来很啰嗦。生成器能把这个压缩成三行。

19.3 生成器函数

函数里出现 yield,它就变成了生成器函数

python
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)      # 3 2 1

执行流程

  1. 调用 countdown(3) 不执行任何代码,只返回一个生成器对象
  2. 每次 next() 时函数从上次 yield 的地方继续执行
  3. 执行到下一个 yield 时暂停,把值交出去
  4. 函数结束时自动抛 StopIteration
python
def demo():
    print("开始")
    yield 1
    print("中间")
    yield 2
    print("结束")

g = demo()          # 什么都不打印
next(g)             # 打印"开始",返回 1
next(g)             # 打印"中间",返回 2
next(g)             # 打印"结束",抛 StopIteration

19.4 生成器的价值:惰性求值

省内存

python
# ❌ 一次性生成 1000 万个数,占几百 MB
def squares_list(n):
    return [i ** 2 for i in range(n)]

# ✅ 用一个算一个,内存恒定
def squares_gen(n):
    for i in range(n):
        yield i ** 2

sum(squares_gen(10_000_000))    # 内存占用几乎为零

处理无限序列

python
def naturals():
    n = 0
    while True:
        yield n
        n += 1

from itertools import islice
list(islice(naturals(), 5))     # [0, 1, 2, 3, 4]

流式处理大文件

python
def read_large_file(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip()

def filter_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def parse(lines):
    for line in lines:
        yield line.split("|")

# 组成流水线,全程只占一行的内存
for parts in parse(filter_errors(read_large_file("huge.log"))):
    print(parts)

这是生成器最漂亮的用法——把处理逻辑拆成一串小生成器,像 Unix 管道一样组合。

19.5 yield from

把迭代委托给另一个可迭代对象:

python
def chain(*iterables):
    for it in iterables:
        yield from it              # 等价于 for x in it: yield x

list(chain([1, 2], "ab"))          # [1, 2, 'a', 'b']

递归时特别有用:

python
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item

list(flatten([1, [2, [3, [4]]], 5]))    # [1, 2, 3, 4, 5]

19.6 itertools:迭代器工具箱

标准库里一组高效的迭代器工具,全部惰性求值:

python
from itertools import (count, cycle, repeat, chain, islice,
                       groupby, product, permutations, combinations,
                       accumulate, takewhile, dropwhile, tee, zip_longest)

# 无限迭代器
count(10, 2)                 # 10, 12, 14, ...
cycle("ABC")                 # A B C A B C ...
repeat("x", 3)               # x x x

# 组合
chain([1,2], [3,4])          # 1 2 3 4
islice(range(100), 5, 10)    # 5 6 7 8 9   (切片,但用于任意迭代器)
zip_longest([1,2], "abc", fillvalue=None)   # (1,'a') (2,'b') (None,'c')

# 排列组合
list(product("AB", repeat=2))       # AA AB BA BB
list(permutations("ABC", 2))        # AB AC BA BC CA CB
list(combinations("ABC", 2))        # AB AC BC

# 累积
list(accumulate([1, 2, 3, 4]))            # [1, 3, 6, 10]  前缀和
list(accumulate([1, 2, 3], func=max))     # 累积最大值

# 条件截取
list(takewhile(lambda x: x < 3, [1,2,3,1]))   # [1, 2]  遇到假就停
list(dropwhile(lambda x: x < 3, [1,2,3,1]))   # [3, 1]  跳过开头

# 分组(⚠️ 必须先按相同的键排序!)
data = sorted(people, key=lambda p: p["city"])
for city, group in groupby(data, key=lambda p: p["city"]):
    print(city, list(group))

⚠️ groupby 只对相邻的相同元素分组,用之前必须先排序,否则结果不是你想的那样。需要真正的分组用 defaultdict

19.7 生成器的高级用法

send():向生成器发送值

python
def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)          # 启动,返回 0
acc.send(10)       # 10
acc.send(5)        # 15

这个用得少,了解即可。异步编程(第 24 章)的底层机制和它相关。

19.8 本章练习

练习 19.1 写一个生成器 fibonacci(),无限产出斐波那契数列。用 islice 取前 10 个。

练习 19.2 写一个生成器 chunks(iterable, size),把序列切成固定大小的块:chunks([1,2,3,4,5], 2)[1,2] [3,4] [5]

练习 19.3 下面代码为什么第二次是空的?

python
data = zip([1,2,3], "abc")
print(list(data))
print(list(data))

练习 19.4 用生成器实现:读取一个日志文件,只返回包含 "ERROR" 的行,并且只返回前 100 条。

练习 19.5 比较下面两种写法的内存占用差别,解释为什么:

python
sum([x**2 for x in range(10**7)])
sum(x**2 for x in range(10**7))

<details> <summary>参考答案</summary>

19.1

python
from itertools import islice

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

list(islice(fibonacci(), 10))    # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

19.2

python
from itertools import islice

def chunks(iterable, size):
    it = iter(iterable)
    while chunk := list(islice(it, size)):
        yield chunk

list(chunks([1,2,3,4,5], 2))     # [[1, 2], [3, 4], [5]]

Python 3.12+ 有内置的 itertools.batched

python
from itertools import batched
list(batched([1,2,3,4,5], 2))    # [(1, 2), (3, 4), (5,)]

19.3 zip 返回迭代器,是一次性的。第一次 list() 已经消耗完了。需要多次使用就先存下来:

python
data = list(zip([1,2,3], "abc"))

19.4

python
from itertools import islice

def error_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if "ERROR" in line:
                yield line.rstrip()

for line in islice(error_lines("app.log"), 100):
    print(line)

关键:文件不会被整个读进内存,而且找够 100 条就停止读取。

19.5

  • 第一行先创建一个含 1000 万个整数的列表(约 400 MB),再求和
  • 第二行用生成器,一次只在内存里保留一个数(几十字节)

结果相同,内存差了七个数量级。函数唯一参数是生成器表达式时括号可省略,所以第二种写法几乎没有额外成本——这应该是默认习惯。 </details>


20. 装饰器

20.1 装饰器是什么

装饰器是"接收一个函数、返回一个新函数"的函数。用途是在不修改原函数代码的前提下给它加功能

python
def my_decorator(func):
    def wrapper():
        print("调用前")
        func()
        print("调用后")
    return wrapper


@my_decorator
def say_hello():
    print("Hello")

say_hello()
# 调用前
# Hello
# 调用后

@my_decorator 只是语法糖,等价于:

python
say_hello = my_decorator(say_hello)

20.2 处理参数和返回值

上面的 wrapper 不接受参数,装饰有参数的函数就会报错。通用写法:

python
import functools

def my_decorator(func):
    @functools.wraps(func)                    # 保留原函数的元信息
    def wrapper(*args, **kwargs):             # 接受任意参数
        print(f"调用 {func.__name__}")
        result = func(*args, **kwargs)        # 原样转发
        print(f"返回 {result}")
        return result                         # 别忘了返回!
    return wrapper


@my_decorator
def add(a, b):
    return a + b

add(1, 2)

⚠️ `@functools.wraps(func)` 不要忘。没有它,被装饰函数的 __name____doc__、类型注解全会变成 wrapper 的,调试和文档工具会失效:

python
add.__name__     # 有 wraps → 'add';没有 → 'wrapper'

⚠️ 别忘了 `return result`。忘了的话所有被装饰的函数都会返回 None——这个 bug 很难查。

20.3 实用装饰器示例

计时

python
import time, functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s")
    return wrapper

重试

python
def retry(times=3, delay=1, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last = e
                    print(f"第 {attempt}/{times} 次失败:{e}")
                    if attempt < times:
                        time.sleep(delay)
            raise last
        return wrapper
    return decorator


@retry(times=3, delay=2, exceptions=(ConnectionError,))
def fetch(url):
    ...

注意带参数的装饰器需要三层嵌套:最外层收装饰器参数,中间层收函数,内层是真正的 wrapper。

日志

python
import logging

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info("调用 %s(%r, %r)", func.__name__, args, kwargs)
        try:
            result = func(*args, **kwargs)
            logging.info("%s 返回 %r", func.__name__, result)
            return result
        except Exception:
            logging.exception("%s 抛出异常", func.__name__)
            raise
    return wrapper

20.4 标准库自带的装饰器

`functools.cache` / `lru_cache`——缓存结果

python
from functools import cache, lru_cache

@cache                        # Python 3.9+,无上限缓存
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

fib(100)          # 瞬间,没有缓存要算到天荒地老

@lru_cache(maxsize=128)       # 限制缓存条数,超出淘汰最久未用的
def expensive_query(user_id):
    ...

expensive_query.cache_info()      # 查看命中率
expensive_query.cache_clear()     # 清空

⚠️ 参数必须可哈希(不能传列表/字典)。缓存的函数必须是纯函数(相同输入永远相同输出),否则会返回过期数据。

`functools.cached_property`——缓存属性

python
from functools import cached_property

class Dataset:
    @cached_property
    def stats(self):
        print("计算中...")        # 只会打印一次
        return heavy_computation(self.data)

d = Dataset()
d.stats     # 计算中... 然后返回
d.stats     # 直接返回缓存

`@property` / `@staticmethod` / `@classmethod`——第 16 章讲过,它们也是装饰器。

`@dataclass`——第 18 章讲过。

`functools.singledispatch`——按类型分派

python
from functools import singledispatch

@singledispatch
def describe(obj):
    return f"未知类型:{obj}"

@describe.register
def _(obj: int):
    return f"整数 {obj}"

@describe.register
def _(obj: list):
    return f"含 {len(obj)} 个元素的列表"

describe(42)        # '整数 42'
describe([1,2])     # '含 2 个元素的列表'

20.5 类装饰器与装饰类

用类实现装饰器(需要保存状态时更清晰):

python
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)


@CountCalls
def hello():
    print("hi")

hello(); hello()
hello.count        # 2

装饰类(装饰器也能作用于类):

python
def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls


@add_repr
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

Point(1, 2)        # Point(x=1, y=2)

20.6 多个装饰器

python
@a
@b
@c
def f(): ...

# 等价于 f = a(b(c(f)))

从下往上应用,从上往下执行。顺序很重要:

python
@app.route("/users")       # 路由必须在最外层
@login_required            # 先检查登录
@timer                     # 再计时
def get_users(): ...

20.7 本章练习

练习 20.1 写一个装饰器 @debug,打印函数名、参数和返回值。

练习 20.2 写一个装饰器 @validate_positive,检查所有位置参数都是正数,否则抛 ValueError。

练习 20.3 下面的装饰器有两个 bug,找出来:

python
def logger(func):
    def wrapper(*args):
        print(f"调用 {func.__name__}")
        func(*args)
    return wrapper

练习 20.4 写一个带参数的装饰器 @rate_limit(calls_per_second=2),限制函数调用频率。

练习 20.5@cache 优化下面的函数,并解释为什么会快:

python
def count_paths(m, n):
    if m == 1 or n == 1:
        return 1
    return count_paths(m-1, n) + count_paths(m, n-1)

<details> <summary>参考答案</summary>

20.1

python
import functools

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        sig = ", ".join([*map(repr, args), *(f"{k}={v!r}" for k, v in kwargs.items())])
        print(f"→ {func.__name__}({sig})")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} 返回 {result!r}")
        return result
    return wrapper

20.2

python
def validate_positive(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for i, a in enumerate(args):
            if isinstance(a, (int, float)) and a <= 0:
                raise ValueError(f"{func.__name__} 的第 {i+1} 个参数必须为正数,得到 {a}")
        return func(*args, **kwargs)
    return wrapper

20.3 两个 bug:

  1. 没有 return func(*args) —— 被装饰的函数永远返回 None
  2. 没有 @functools.wraps(func) —— 丢失原函数元信息

另外 wrapper(*args) 不接受关键字参数,也算问题。修正:

python
def logger(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

20.4

python
import time, functools

def rate_limit(calls_per_second=2):
    min_interval = 1.0 / calls_per_second
    def decorator(func):
        last_called = 0.0
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal last_called
            elapsed = time.perf_counter() - last_called
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            last_called = time.perf_counter()
            return func(*args, **kwargs)
        return wrapper
    return decorator

20.5

python
from functools import cache

@cache
def count_paths(m, n):
    if m == 1 or n == 1:
        return 1
    return count_paths(m-1, n) + count_paths(m, n-1)

count_paths(18, 18)    # 瞬间返回

原因:朴素递归会重复计算同一个 (m, n) 组合指数级次数(比如 count_paths(3,3) 会算好几遍 count_paths(2,2))。加了缓存后每个组合只算一次,复杂度从 O(2^(m+n)) 降到 O(m×n)。 </details>


21. 类型注解

Python 是动态类型语言,类型注解不影响运行——它是给人和工具看的。但在超过几百行的项目里,它的价值极大。

21.1 基本语法

python
name: str = "张三"
age: int = 25
scores: list[int] = [90, 85]

def greet(name: str, times: int = 1) -> str:
    return f"你好,{name}!" * times

⚠️ 注解不会被检查

python
def add(a: int, b: int) -> int:
    return a + b

add("x", "y")     # 'xy'  —— 照样能跑,Python 不管

要真正检查需要类型检查器(21.6 节)。

21.2 常用类型

python
from typing import Any, Optional, Union, Callable, Iterator, TypeVar

# 基本类型直接写
x: int
y: float
s: str
b: bool

# 容器(Python 3.9+ 用内置类型,不用再 from typing import List)
nums: list[int]
pairs: dict[str, int]
point: tuple[float, float]           # 固定长度、每项类型不同
row: tuple[int, ...]                 # 任意长度、同类型
tags: set[str]

# 可能为 None
name: str | None = None              # Python 3.10+ 推荐写法
name: Optional[str] = None           # 旧写法,等价

# 多种类型之一
value: int | str
value: Union[int, str]               # 旧写法

# 函数类型
handler: Callable[[int, str], bool]  # 接收 int 和 str,返回 bool
callback: Callable[..., None]        # 任意参数,返回 None

# 任意类型(相当于关掉检查,慎用)
data: Any

21.3 复杂一点的场景

泛型函数

python
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    return items[0] if items else None

first([1, 2, 3])       # 类型检查器知道返回 int | None
first(["a", "b"])      # 知道返回 str | None

Python 3.12+ 有更简洁的语法:

python
def first[T](items: list[T]) -> T | None:
    return items[0] if items else None

类中的注解

python
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str | None = None

    def is_adult(self) -> bool:
        return self.age >= 18

协议(结构化类型)——描述"有什么方法"而不是"继承自谁",这才是符合鸭子类型的做法:

python
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(item: Drawable) -> None:      # 任何有 draw 方法的对象都行
    item.draw()

类型别名

python
type UserId = int                        # Python 3.12+
type JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None

UserId = int                             # 旧写法

特殊返回类型

python
from typing import NoReturn

def fail(msg: str) -> NoReturn:          # 永远不正常返回
    raise RuntimeError(msg)

def gen() -> Iterator[int]:              # 生成器
    yield 1

21.4 注解的实用价值

1. IDE 自动补全

python
def process(user):          # IDE 不知道 user 有什么方法
    user.          # ← 没有补全提示

def process(user: User):    # IDE 知道了
    user.          # ← 弹出 name, age, is_adult...

2. 文档作用

python
# 不看实现能看懂吗?
def calc(a, b, c): ...

# 一目了然
def calculate_discount(price: Decimal, rate: float, is_vip: bool) -> Decimal: ...

3. 提前发现 bug

类型检查器能在写代码时就发现 None 没处理、参数传反了、返回值类型不对。

21.5 什么时候写注解

一定要写

  • 公开的函数/方法签名
  • 复杂的数据结构
  • 团队协作或长期维护的项目

可以不写

  • 一次性脚本
  • 显而易见的局部变量(count = 0 不需要写 count: int = 0
  • 私有的小辅助函数

💡 渐进式采用:不需要一次性给整个项目加注解。从新代码和核心模块开始,逐步覆盖。

21.6 类型检查器

mypy——最成熟的选择

bash
pip install mypy
mypy your_file.py

pyright / basedpyright——微软出品,速度快,VS Code 的 Pylance 就基于它

ty / pyrefly——2025 年发布的新一代检查器,用 Rust 写,速度比 mypy 快一到两个数量级。但截至 2026 年年中仍处于 beta 阶段,类型推断的覆盖面和错误信息质量还不如 mypy/pyright 成熟,可以试用,不建议现在就作为项目的唯一门禁

配置(写在 pyproject.toml):

toml
[tool.mypy]
python_version = "3.12"
strict = true                    # 严格模式,新项目推荐
warn_return_any = true
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = "some_untyped_lib.*"
ignore_missing_imports = true

忽略某一行

python
result = weird_call()  # type: ignore[attr-defined]

21.7 运行时也想校验怎么办

类型注解不检查,需要运行时校验用 pydantic

python
from pydantic import BaseModel, Field, field_validator

class User(BaseModel):
    name: str = Field(min_length=1, max_length=50)
    age: int = Field(ge=0, le=150)
    email: str

    @field_validator("email")
    @classmethod
    def check_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("邮箱格式不正确")
        return v


User(name="张三", age=25, email="[email protected]")     # ✅
User(name="", age=200, email="bad")           # ValidationError,一次报出所有问题

分工原则

  • 内部代码用 dataclass + 类型注解 + mypy(零运行时开销)
  • 处理外部输入(HTTP、配置文件、CLI 参数)用 pydantic(真正校验)

21.8 本章练习

练习 21.1 给下面的函数加上完整的类型注解:

python
def find_users(names, min_age=0, active_only=True):
    return [u for u in database if u.name in names]

练习 21.2 写出下面数据结构的类型注解:

  • 一个字符串到整数列表的字典
  • 一个可能是字符串也可能是 None 的值
  • 一个接收字符串返回布尔值的函数
  • 一个包含 (名字, 年龄, 是否激活) 三元组的列表

练习 21.3 下面的注解有什么问题?

python
def get_config() -> dict:
    return {"host": "localhost", "port": 8080}

练习 21.4 用 Protocol 定义一个"可以被序列化"的接口,要求有 to_dict() 方法。

<details> <summary>参考答案</summary>

21.1

python
from collections.abc import Sequence

def find_users(
    names: Sequence[str],
    min_age: int = 0,
    active_only: bool = True,
) -> list[User]:
    return [u for u in database if u.name in names]

参数用 Sequence[str] 而不是 list[str] 更宽松——调用方传元组也行。这是"接收时宽松,返回时具体"的原则。

21.2

python
d: dict[str, list[int]]
s: str | None
f: Callable[[str], bool]
records: list[tuple[str, int, bool]]

21.3 dict 太笼统,没有说明键值类型。更好的写法:

python
def get_config() -> dict[str, str | int]: ...

# 更好:用 TypedDict 或 dataclass 明确结构
from typing import TypedDict

class Config(TypedDict):
    host: str
    port: int

def get_config() -> Config: ...

21.4

python
from typing import Protocol, Any

class Serializable(Protocol):
    def to_dict(self) -> dict[str, Any]: ...


def save_all(items: list[Serializable]) -> None:
    for item in items:
        write(item.to_dict())

任何有 to_dict() 方法的类都满足这个协议,不需要显式继承。 </details>


22. 标准库巡览

Python 自带的库覆盖面极广。这一章挑最常用的过一遍——不需要背,知道"有这么个东西"就够了,用到时再查文档。

22.1 日期与时间

python
from datetime import datetime, date, time, timedelta, timezone

now = datetime.now()                    # 本地当前时间
utc = datetime.now(timezone.utc)        # UTC 时间(推荐存储用这个)
today = date.today()

# 构造
d = date(2026, 7, 26)
dt = datetime(2026, 7, 26, 14, 30, 0)

# 格式化
now.strftime("%Y-%m-%d %H:%M:%S")       # '2026-07-26 14:30:00'
now.isoformat()                         # '2026-07-26T14:30:00.123456'

# 解析
datetime.strptime("2026-07-26", "%Y-%m-%d")
datetime.fromisoformat("2026-07-26T14:30:00")

# 计算
tomorrow = today + timedelta(days=1)
delta = date(2026, 12, 31) - today
delta.days                              # 剩余天数

# 时间戳
now.timestamp()                         # 1785…
datetime.fromtimestamp(1785000000)

常用格式符%Y%m%d%H%M%S%A 星期 %B 月份名

⚠️ 时区是最大的坑。原则:

  • 存储和传输永远用 UTC
  • 只在展示给用户时转成本地时区
  • 需要复杂时区处理用 zoneinfo(Python 3.9+ 内置)
python
from zoneinfo import ZoneInfo

beijing = datetime.now(ZoneInfo("Asia/Shanghai"))
utc_time = beijing.astimezone(timezone.utc)

22.2 时间测量

python
import time

time.time()               # Unix 时间戳(会被系统调时影响)
time.perf_counter()       # 高精度计时,测量耗时用这个
time.monotonic()          # 单调时钟,不会倒退
time.sleep(1.5)           # 暂停

# 测量代码耗时
start = time.perf_counter()
do_work()
print(f"{time.perf_counter() - start:.4f}s")

微基准测试用 timeit

python
import timeit
timeit.timeit("'-'.join(str(n) for n in range(100))", number=10000)

22.3 随机

python
import random

random.random()                     # [0.0, 1.0) 之间的浮点数
random.randint(1, 6)                # 1-6 之间的整数(含两端)
random.randrange(0, 10, 2)          # 0,2,4,6,8 中随机一个
random.uniform(1.5, 3.5)            # 区间内随机浮点数
random.choice(["a", "b", "c"])      # 随机选一个
random.choices(items, k=3)          # 有放回抽 3 个
random.choices(items, weights=[1,5,2], k=3)   # 带权重
random.sample(items, 3)             # 无放回抽 3 个
random.shuffle(lst)                 # 原地打乱

random.seed(42)                     # 固定种子,结果可复现(测试有用)

⚠️ `random` 不是密码学安全的。生成密码、令牌、密钥必须用 secrets

python
import secrets

secrets.token_hex(16)               # '3d8f2a...'  随机十六进制字符串
secrets.token_urlsafe(32)           # URL 安全的随机串
secrets.choice(alphabet)            # 安全的随机选择
secrets.compare_digest(a, b)        # 恒定时间比较,防时序攻击

22.4 数学

python
import math

math.pi, math.e, math.inf, math.nan
math.sqrt(16)          # 4.0
math.floor(3.7)        # 3    向下取整
math.ceil(3.2)         # 4    向上取整
math.trunc(-3.7)       # -3   向零截断
math.factorial(5)      # 120
math.gcd(12, 18)       # 6    最大公约数
math.lcm(4, 6)         # 12   最小公倍数(3.9+)
math.log(100, 10)      # 2.0
math.hypot(3, 4)       # 5.0  欧几里得距离
math.isclose(a, b)     # 浮点数近似比较
math.isnan(x), math.isinf(x)
math.comb(5, 2)        # 10   组合数
math.dist((0,0), (3,4))  # 5.0

统计用 statistics

python
import statistics as st

st.mean([1,2,3,4])       # 2.5    平均数
st.median([1,2,3,4])     # 2.5    中位数
st.mode([1,1,2])         # 1      众数
st.stdev([1,2,3,4])      # 样本标准差
st.pstdev([1,2,3,4])     # 总体标准差
st.quantiles(data, n=4)  # 四分位数

22.5 系统与进程

python
import os, sys, platform

os.environ.get("HOME")           # 读环境变量
os.environ["MY_VAR"] = "x"       # 设置(只影响当前进程)
os.cpu_count()                   # CPU 核数
os.getpid()                      # 进程 ID

sys.argv                         # 命令行参数列表,argv[0] 是脚本名
sys.exit(1)                      # 退出,非 0 表示出错
sys.platform                     # 'darwin' / 'win32' / 'linux'
sys.version_info                 # (3, 14, 6, 'final', 0)
sys.stdout, sys.stderr, sys.stdin

platform.system()                # 'Darwin' / 'Windows' / 'Linux'

运行外部命令

python
import subprocess

# 推荐写法:传列表(自动处理转义),检查返回码,捕获输出
result = subprocess.run(
    ["git", "status", "--short"],
    capture_output=True,
    text=True,                  # 输出当字符串处理而不是 bytes
    check=True,                 # 返回码非 0 时抛异常
)
print(result.stdout)

⚠️ 永远不要用 `shell=True` 拼接用户输入,那是命令注入漏洞:

python
subprocess.run(f"rm {filename}", shell=True)     # ❌ filename="; rm -rf /" 就完了
subprocess.run(["rm", filename])                 # ✅

22.6 命令行参数

python
import argparse

parser = argparse.ArgumentParser(description="批量处理文件")
parser.add_argument("input", help="输入文件路径")
parser.add_argument("-o", "--output", default="out.txt", help="输出路径")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
parser.add_argument("-n", "--count", type=int, default=10)
parser.add_argument("--mode", choices=["fast", "safe"], default="safe")

args = parser.parse_args()
print(args.input, args.output, args.verbose)

自动生成 --help,自动校验类型。

💡 更现代的选择:Typer(基于类型注解,代码量少一半)或 Click。需要 pip 安装。

22.7 文本处理

python
import textwrap, string, difflib, unicodedata

textwrap.fill(long_text, width=70)       # 自动折行
textwrap.dedent(indented_text)           # 去掉公共缩进(写多行字符串很有用)
textwrap.shorten(text, width=50)         # 截断并加省略号

string.ascii_lowercase                   # 'abcdefghijklmnopqrstuvwxyz'
string.digits, string.punctuation

difflib.get_close_matches("aple", ["apple", "banana"])   # ['apple'] 模糊匹配
difflib.unified_diff(lines1, lines2)                     # 生成 diff

22.8 数据处理

python
import json, csv, sqlite3, pickle, base64, hashlib, uuid

# 哈希
hashlib.sha256(b"hello").hexdigest()
hashlib.md5(data).hexdigest()            # ⚠️ md5 已不安全,别用于密码

# 密码存储必须用专门的算法(第三方库)
# pip install argon2-cffi  或  bcrypt

# UUID
uuid.uuid4()                             # 随机唯一 ID
str(uuid.uuid4())                        # 'f47ac10b-58cc-...'

# Base64
base64.b64encode(b"data").decode()
base64.b64decode("ZGF0YQ==")

# SQLite(零配置的本地数据库,标准库自带)
conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO users (name) VALUES (?)", ("张三",))   # ⚠️ 一定用参数化
conn.commit()
for row in conn.execute("SELECT * FROM users"):
    print(row)
conn.close()

⚠️ SQL 必须用参数化(? 占位符),绝不能用 f-string 拼 SQL——那是 SQL 注入。

22.9 网络

python
import urllib.request, urllib.parse

# 标准库能用,但很啰嗦
with urllib.request.urlopen("https://api.example.com/data") as r:
    data = json.loads(r.read())

urllib.parse.urlencode({"q": "python", "page": 1})    # 'q=python&page=1'
urllib.parse.quote("中文")                             # URL 编码

💡 实际项目请用第三方库,体验天差地别:

python
# pip install httpx
import httpx

r = httpx.get("https://api.example.com/data", params={"q": "python"}, timeout=10)
r.raise_for_status()
data = r.json()

# POST JSON
r = httpx.post(url, json={"name": "张三"}, headers={"Authorization": "Bearer ..."})

requests 是老牌选择,httpx 是现代替代——API 几乎一样,额外支持异步。HTTP/2 也支持,但需要装可选依赖:pip install "httpx[http2]",并在创建客户端时显式开启 httpx.Client(http2=True)

22.10 函数式工具

python
import functools, operator

functools.reduce(operator.add, [1,2,3,4])     # 10  (一般用 sum 就好)
functools.partial(int, base=2)("1010")        # 10  固定部分参数

operator.itemgetter(1)                        # 取索引 1,用于 key=
operator.attrgetter("name")                   # 取属性
operator.methodcaller("upper")                # 调用方法

partial 的实用场景:

python
from functools import partial

# 创建一个"预配置"的函数
save_json = partial(json.dump, ensure_ascii=False, indent=2)
save_json(data, f)      # 不用每次都写那两个参数

22.11 其他值得知道的

python
import copy            # deepcopy
import glob            # 文件通配(pathlib.glob 更好用)
import tempfile        # 临时文件/目录
import zipfile, tarfile, gzip     # 压缩
import shutil          # 高级文件操作
import warnings        # 发出警告
import inspect         # 运行时检查对象(写框架时用)
import weakref         # 弱引用
import decimal, fractions         # 精确数值
import heapq           # 堆/优先队列
import bisect          # 有序列表二分查找
import array           # 紧凑数值数组
import struct          # 二进制打包
import html, xml       # 标记语言
import email, smtplib  # 邮件
import calendar        # 日历
import pprint          # 美化打印嵌套结构

heapq 的实用场景——找 top N:

python
import heapq
heapq.nlargest(3, data, key=lambda x: x["score"])
heapq.nsmallest(3, data)

tempfile——需要临时文件时别自己在 /tmp 造名字:

python
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
    path = Path(tmpdir) / "work.txt"
    ...    # 退出 with 时自动删除整个目录

22.12 本章练习

练习 22.1 写一个程序,计算今天距离今年年底还有多少天。

练习 22.2 生成一个 12 位的安全随机密码,包含大小写字母和数字。

练习 22.3 写一个命令行工具:接收一个目录路径参数和一个 --ext 选项,统计该目录下指定扩展名的文件数量。

练习 22.4 用 sqlite3 创建一个待办事项数据库,实现添加、列出、标记完成三个功能。

练习 22.5 下面代码有安全问题,指出并修复:

python
import subprocess
filename = input("文件名:")
subprocess.run(f"cat {filename}", shell=True)

<details> <summary>参考答案</summary>

22.1

python
from datetime import date

today = date.today()
year_end = date(today.year, 12, 31)
print(f"距离 {today.year} 年底还有 {(year_end - today).days} 天")

22.2

python
import secrets, string

alphabet = string.ascii_letters + string.digits
password = "".join(secrets.choice(alphabet) for _ in range(12))
print(password)

secrets 而不是 random——后者的伪随机数是可预测的。

22.3

python
import argparse
from pathlib import Path

parser = argparse.ArgumentParser(description="统计文件数量")
parser.add_argument("directory", type=Path)
parser.add_argument("--ext", default="py", help="扩展名,不带点")
args = parser.parse_args()

files = list(args.directory.rglob(f"*.{args.ext}"))
print(f"{args.directory} 下有 {len(files)} 个 .{args.ext} 文件")

22.4

python
import sqlite3
from contextlib import closing

def init(conn):
    conn.execute("""
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            done INTEGER DEFAULT 0
        )
    """)
    conn.commit()

def add(conn, title):
    conn.execute("INSERT INTO todos (title) VALUES (?)", (title,))
    conn.commit()

def list_all(conn):
    for row in conn.execute("SELECT id, title, done FROM todos ORDER BY id"):
        mark = "✓" if row[2] else " "
        print(f"[{mark}] {row[0]}. {row[1]}")

def complete(conn, todo_id):
    conn.execute("UPDATE todos SET done = 1 WHERE id = ?", (todo_id,))
    conn.commit()

with closing(sqlite3.connect("todo.db")) as conn:
    init(conn)
    add(conn, "学习 Python")
    add(conn, "写练习题")
    complete(conn, 1)
    list_all(conn)

22.5 shell=True 加上未经处理的用户输入 = 命令注入。用户输入 a.txt; rm -rf ~ 就会执行删除命令。

python
import subprocess
filename = input("文件名:")
subprocess.run(["cat", filename], check=True)     # ✅ 不经过 shell

更好的做法是根本不调用外部命令:

python
from pathlib import Path
print(Path(filename).read_text(encoding="utf-8"))

</details>


23. 正则表达式

正则表达式是描述文本模式的小语言。它很强大,也很容易写出没人看得懂的东西。

23.1 基本用法

python
import re

text = "我的电话是 138-1234-5678,备用 139-8765-4321"

re.search(r"\d{3}-\d{4}-\d{4}", text)      # 找第一个匹配,返回 Match 对象或 None
re.findall(r"\d{3}-\d{4}-\d{4}", text)     # 找全部,返回列表
re.finditer(r"\d+", text)                  # 找全部,返回迭代器(能拿到位置)
re.sub(r"\d", "*", text)                   # 替换
re.split(r"[,,]", text)                    # 按模式分割
re.fullmatch(r"\d+", "123")                # 整个字符串必须匹配
re.match(r"我的", text)                     # 只从开头匹配

⚠️ 正则字符串永远用 `r"..."`(原始字符串),否则 \d 会被 Python 先解释成转义序列。

Match 对象

python
m = re.search(r"(\d{3})-(\d{4})", text)
if m:
    m.group()      # '138-1234'   整个匹配
    m.group(1)     # '138'        第一个捕获组
    m.group(2)     # '1234'
    m.groups()     # ('138', '1234')
    m.start()      # 匹配起始位置
    m.span()       # (6, 14)

23.2 语法速查

字符类

.        任意字符(除换行)
\d       数字        \D  非数字
\w       字母数字下划线  \W  非
\s       空白字符     \S  非
[abc]    a 或 b 或 c
[^abc]   除了 abc
[a-z]    a 到 z
[0-9a-fA-F]  十六进制字符

数量

*        0 次或多次
+        1 次或多次
?        0 次或 1 次
{3}      恰好 3 次
{2,5}    2 到 5 次
{2,}     至少 2 次
*?  +?  ??  {n,m}?    非贪婪版本(尽可能少匹配)

位置

^        字符串开头(多行模式下是行首)
$        字符串结尾
\b       单词边界
\B       非单词边界

分组

(...)         捕获组
(?:...)       非捕获组(只分组不捕获,效率更高)
(?P<name>...) 命名组
(?=...)       正向先行断言(后面必须是)
(?!...)       负向先行断言(后面不能是)
(?<=...)      正向后行断言(前面必须是)
(?<!...)      负向后行断言
|             或

23.3 贪婪 vs 非贪婪

这是最常见的困惑来源:

python
text = "<b>粗体</b><i>斜体</i>"

re.findall(r"<.*>", text)      # ['<b>粗体</b><i>斜体</i>']  贪婪:匹配到最后
re.findall(r"<.*?>", text)     # ['<b>', '</b>', '<i>', '</i>']  非贪婪 ✅

默认 *+ 都是贪婪的——尽可能多匹配。加 ? 变成非贪婪。

23.4 常用模式

python
# 邮箱(简化版,完整的邮箱正则极其复杂,实际用库校验)
r"[\w.+-]+@[\w-]+\.[\w.]+"

# 中国手机号
r"1[3-9]\d{9}"

# URL
r"https?://[^\s]+"

# IPv4
r"\b(?:\d{1,3}\.){3}\d{1,3}\b"

# 日期 YYYY-MM-DD
r"\d{4}-\d{2}-\d{2}"

# 中文字符
r"[一-鿿]+"

# HTML 标签(简单场景;解析 HTML 请用 Beautiful Soup)
r"<[^>]+>"

# 提取引号内的内容
r'"([^"]*)"'

23.5 命名组与替换

python
# 命名组让代码可读得多
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
m = re.search(pattern, "日期:2026-07-26")
m.group("year")      # '2026'
m.groupdict()        # {'year': '2026', 'month': '07', 'day': '26'}

# 替换中引用分组
re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "2026-07-26")   # '26/07/2026'
re.sub(pattern, r"\g<day>/\g<month>/\g<year>", text)            # 用名字引用

# 替换用函数(复杂逻辑)
def upper_match(m):
    return m.group().upper()

re.sub(r"\b\w{5,}\b", upper_match, "hello world hi")   # 'HELLO WORLD hi'

23.6 标志

python
re.IGNORECASE  / re.I    # 忽略大小写
re.MULTILINE   / re.M    # ^ $ 匹配每行的首尾
re.DOTALL      / re.S    # . 也匹配换行符
re.VERBOSE     / re.X    # 允许写注释和空白

re.search(r"hello", text, re.IGNORECASE)

re.VERBOSE 能让复杂正则变得可读:

python
pattern = re.compile(r"""
    (\d{3})     # 区号
    [-\s]?      # 可选分隔符
    (\d{4})     # 前四位
    [-\s]?
    (\d{4})     # 后四位
""", re.VERBOSE)

23.7 预编译

同一个正则要用很多次时,先编译能提速:

python
pattern = re.compile(r"\d+")

pattern.search(text)
pattern.findall(text)
pattern.sub("X", text)

23.8 什么时候不该用正则

别用正则解析结构化格式

python
# ❌ 用正则解析 HTML/XML/JSON —— 这些是嵌套结构,正则表达不了
re.findall(r"<div>(.*?)</div>", html)

# ✅ 用专门的解析器
from bs4 import BeautifulSoup      # HTML
import json                         # JSON
import csv                          # CSV

简单场景别用正则

python
re.search(r"^abc", s)          → s.startswith("abc")
re.search(r"abc", s)           → "abc" in s
re.sub(r"abc", "x", s)         → s.replace("abc", "x")
re.split(r",", s)              → s.split(",")

字符串方法更快、更清晰。

警惕灾难性回溯:某些正则在特定输入上会指数级变慢,可能被用来做拒绝服务攻击。

python
r"(a+)+b"     # 对 "aaaaaaaaaaaaaaaaaaaaac" 会卡死

避免嵌套的不定长量词。处理不可信输入时要格外小心。

23.9 本章练习

练习 23.1 写一个正则,从文本中提取所有邮箱地址。

练习 23.2"2026-07-26" 格式的日期批量替换成 "2026年07月26日"

练习 23.3 写一个函数,校验密码强度:至少 8 位,包含大写、小写、数字各至少一个。

练习 23.4 下面这个正则想匹配 HTML 标签内容,为什么结果不对?怎么改?

python
re.findall(r"<b>(.*)</b>", "<b>a</b> and <b>b</b>")

练习 23.5 从一段日志中提取所有 IP 地址和时间戳,输出成字典列表。日志格式:192.168.1.1 - [2026-07-26 14:30:00] "GET /api"

<details> <summary>参考答案</summary>

23.1

python
import re
emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", text)

注意这只是实用近似。严格的邮箱校验应该用 email-validator 库,或者干脆发一封验证邮件——那才是唯一可靠的校验。

23.2

python
re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1年\2月\3日", text)

23.3

python
import re

def is_strong(password: str) -> bool:
    if len(password) < 8:
        return False
    checks = [r"[a-z]", r"[A-Z]", r"\d"]
    return all(re.search(c, password) for c in checks)

用先行断言写成一个正则也行,但可读性差很多:

python
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$"

分开检查还有个好处:能告诉用户具体缺什么

23.4 .* 是贪婪的,会从第一个 <b> 一直匹配到最后一个 </b>,结果是 ['a</b> and <b>b']

python
re.findall(r"<b>(.*?)</b>", text)     # ['a', 'b']  ✅ 非贪婪

23.5

python
import re

log = '192.168.1.1 - [2026-07-26 14:30:00] "GET /api"\n10.0.0.5 - [2026-07-26 14:31:02] "POST /login"'

pattern = re.compile(
    r"(?P<ip>\d{1,3}(?:\.\d{1,3}){3}).*?"
    r"\[(?P<timestamp>[\d-]+ [\d:]+)\]"
)

records = [m.groupdict() for m in pattern.finditer(log)]
# [{'ip': '192.168.1.1', 'timestamp': '2026-07-26 14:30:00'}, ...]

</details>


24. 并发与异步

这一章解决的问题是:程序在等待的时候能不能干别的事?

24.1 三种"同时做多件事"的方式

方式模块适合原理
多线程threadingI/O 密集(网络、文件)一个进程内多个线程,共享内存
多进程multiprocessingCPU 密集(计算)多个独立进程,真正并行
异步asyncio大量并发 I/O单线程内协作式切换

判断你的任务是哪种

  • I/O 密集:大部分时间在等(等网络响应、等磁盘、等数据库)。CPU 占用率很低。
  • CPU 密集:大部分时间在算(图像处理、数值计算、加密)。CPU 跑满。

24.2 GIL:为什么 Python 多线程不能加速计算

CPython 有个全局解释器锁(GIL):同一时刻只有一个线程能执行 Python 字节码。

后果:

  • ❌ 多线程无法加速纯计算任务(4 个线程和 1 个线程一样快,甚至更慢)
  • ✅ 多线程加速 I/O 任务(线程等 I/O 时会释放 GIL,让别的线程跑)
🔍 延伸自由线程(free-threaded,即去掉 GIL)构建在 Python 3.13 作为实验特性引入,随 PEP 779 的接受,3.14 起成为正式支持的构建版本——但它仍是可选构建、不是默认,需要单独安装 python3.14tt 表示 threaded)。目前单线程性能相比常规构建仍有个位数到 10% 左右的损失,第三方 C 扩展的适配也还在进行中。所以这一章的实践建议暂时不变;等你的关键依赖都标注支持自由线程了,再考虑迁移。

24.3 多线程

python
from concurrent.futures import ThreadPoolExecutor
import httpx

urls = ["https://example.com/1", "https://example.com/2", ...]

def fetch(url):
    return httpx.get(url, timeout=10).text

# 高层 API,推荐用这个而不是直接操作 Thread
with ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(fetch, urls))

需要处理单个任务的结果和异常:

python
from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=10) as pool:
    futures = {pool.submit(fetch, url): url for url in urls}
    for future in as_completed(futures):        # 谁先完成先处理谁
        url = futures[future]
        try:
            print(url, len(future.result()))
        except Exception as e:
            print(f"{url} 失败:{e}")

线程安全:多个线程改同一个变量会出问题。

python
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:              # 保证同一时刻只有一个线程能执行
            counter += 1

💡 更好的做法是避免共享可变状态:让每个线程返回结果,主线程汇总。queue.Queue 本身是线程安全的,适合做线程间通信。

24.4 多进程

python
from concurrent.futures import ProcessPoolExecutor

def heavy_compute(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":              # ⚠️ Windows/macOS 上必须有这行
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(heavy_compute, [10**7] * 4))

多进程绕过了 GIL,能真正利用多核。代价:

  • 进程创建开销大
  • 数据要在进程间序列化传输(用 pickle),大对象传起来很慢
  • 不能共享内存对象(要用 multiprocessing.Manager 或共享内存)

⚠️ `if __name__ == "__main__"` 不是可选的——没有它,子进程会重新导入主模块,导致无限递归创建进程。

24.5 asyncio:异步编程

python
import asyncio
import httpx

async def fetch(client, url):            # async def 定义协程
    r = await client.get(url)            # await 处让出控制权
    return r.text

async def main():
    async with httpx.AsyncClient() as client:
        tasks = [fetch(client, url) for url in urls]
        results = await asyncio.gather(*tasks)      # 并发执行全部
    return results

asyncio.run(main())                      # 启动事件循环

核心概念

  • async def 定义的函数叫协程,调用它不会执行,而是返回一个协程对象
  • await 表示"这里要等,我先让出 CPU 给别的任务"
  • 事件循环负责在众多协程之间调度
  • asyncio.run() 是程序的异步入口

常用 API

python
await asyncio.sleep(1)                      # 异步睡眠(不阻塞其他任务)
await asyncio.gather(*coros)                # 并发运行,全部完成后返回结果列表
await asyncio.wait_for(coro, timeout=5)     # 超时控制
task = asyncio.create_task(coro)            # 创建后台任务
await task

# Python 3.11+ 的任务组(更安全,异常处理更好)
async with asyncio.TaskGroup() as tg:
    tg.create_task(work1())
    tg.create_task(work2())
# 退出时自动等待所有任务;任一失败会取消其余任务

async 的传染性

python
async def a(): ...
async def b():
    await a()          # 只能在 async 函数里 await
def c():
    await a()          # ❌ SyntaxError

一旦用了 async,调用链上的函数都得是 async。这是 asyncio 最大的成本——不能只在一小块地方用。

⚠️ 不要在异步代码里调用阻塞函数

python
async def bad():
    time.sleep(1)                    # ❌ 阻塞整个事件循环,所有任务都卡住
    requests.get(url)                # ❌ 同上

async def good():
    await asyncio.sleep(1)           # ✅
    await client.get(url)            # ✅ 用异步的 HTTP 客户端

    # 必须调用阻塞函数时,扔到线程池
    result = await asyncio.to_thread(blocking_function, arg)

24.6 怎么选

任务是 CPU 密集的?
├─ 是 → ProcessPoolExecutor(或者用 numpy/Rust 扩展)
└─ 否(I/O 密集)
    ├─ 并发量 < 几十 → ThreadPoolExecutor(简单,不用改代码风格)
    └─ 并发量成百上千 → asyncio(内存效率高得多)

实践建议

  1. 先别用并发。测量之后确认真的是瓶颈再说。过早并发化会让代码复杂度翻倍。
  2. 优先用 `concurrent.futures`,它的 API 统一,从线程池换成进程池只需改一个词。
  3. asyncio 适合从头设计的项目,不适合往同步代码里"加一点"。
  4. 能用批量 API 就别用并发——一次请求 100 个 ID 比并发 100 次请求好得多。

24.7 完整示例:并发下载

python
import asyncio
import httpx
from pathlib import Path

async def download(client: httpx.AsyncClient, url: str, dest: Path,
                   sem: asyncio.Semaphore) -> str:
    async with sem:                       # 限制并发数,别把对方服务器打挂
        try:
            r = await client.get(url, timeout=30, follow_redirects=True)
            r.raise_for_status()
            dest.write_bytes(r.content)
            return f"✅ {url} → {dest.name}"
        except httpx.HTTPError as e:
            return f"❌ {url}:{e}"


async def main(urls: list[str], outdir: Path) -> None:
    outdir.mkdir(parents=True, exist_ok=True)
    sem = asyncio.Semaphore(5)            # 最多 5 个并发

    async with httpx.AsyncClient() as client:
        tasks = [
            download(client, url, outdir / f"{i:03d}.dat", sem)
            for i, url in enumerate(urls)
        ]
        for coro in asyncio.as_completed(tasks):
            print(await coro)


if __name__ == "__main__":
    asyncio.run(main(["https://example.com"] * 10, Path("downloads")))

注意 Semaphore——不加限制地并发几百个请求,轻则被限流,重则被封 IP。

24.8 本章练习

练习 24.1 解释为什么下面代码用多线程不会变快:

python
def compute():
    return sum(i*i for i in range(10**7))

练习 24.2ThreadPoolExecutor 并发读取 10 个文件的内容。

练习 24.3 把下面的同步代码改成 asyncio 版本:

python
import time
def task(n):
    time.sleep(1)
    return n * 2

results = [task(i) for i in range(5)]   # 耗时 5 秒

练习 24.4 下面的异步代码有什么问题?

python
async def fetch_all(urls):
    results = []
    for url in urls:
        results.append(await fetch(url))
    return results

<details> <summary>参考答案</summary>

24.1 这是 CPU 密集任务。由于 GIL 的存在,同一时刻只有一个线程能执行 Python 字节码,多线程只会在线程间切换,不会真正并行计算,反而增加了切换开销。应该用 ProcessPoolExecutor

24.2

python
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

paths = list(Path("data").glob("*.txt"))

def read(p):
    return p.name, p.read_text(encoding="utf-8")

with ThreadPoolExecutor(max_workers=10) as pool:
    for name, content in pool.map(read, paths):
        print(name, len(content))

24.3

python
import asyncio

async def task(n):
    await asyncio.sleep(1)
    return n * 2

async def main():
    return await asyncio.gather(*(task(i) for i in range(5)))

results = asyncio.run(main())    # 耗时约 1 秒

24.4 这段代码是串行的——每次 await fetch(url) 都会等这个请求完成才发下一个,完全没有并发效果,跟同步代码一样慢。

python
async def fetch_all(urls):
    return await asyncio.gather(*(fetch(url) for url in urls))

这是异步编程最常见的错误:`await` 在循环里 = 串行。要并发必须先创建所有协程/任务,再一起 await。 </details>


第 7 部分 · 工程实践

会写代码和能交付软件之间还有一段距离。这一部分讲的东西不会让你的程序多一个功能,但决定了它能不能被别人(和三个月后的你)维护。

25. 虚拟环境与依赖管理

25.1 为什么需要虚拟环境

假设你有两个项目:项目 A 需要 django==3.2,项目 B 需要 django==5.0。如果所有包都装在系统 Python 里,它们会冲突。

虚拟环境给每个项目一份独立的 Python 和包目录。这不是可选的最佳实践,而是必须的

25.2 venv:标准库方案

bash
# 创建(会生成一个 .venv 目录)
python3 -m venv .venv

# 激活
source .venv/bin/activate          # macOS / Linux
.venv\Scripts\activate             # Windows

# 激活后提示符会变成 (.venv) $
# 这时 python 和 pip 都指向虚拟环境里的版本

pip install requests
pip list

deactivate                         # 退出

.venv/ 加进 .gitignore——虚拟环境不进版本控制,别人拿到代码后自己创建。

25.3 pip 常用命令

bash
pip install requests                 # 安装
pip install "django>=4.0,<5.0"       # 指定版本范围
pip install -r requirements.txt      # 从文件批量安装
pip install -e .                     # 以可编辑模式安装本项目
pip install --upgrade requests
pip uninstall requests
pip list                             # 列出已安装
pip list --outdated                  # 列出可升级的
pip show requests                    # 查看详情和依赖
pip freeze > requirements.txt        # 导出当前环境所有包及精确版本

⚠️ pip freeze 会把所有包(含间接依赖)都写进去。更好的做法是手动维护"我直接需要什么",见下节。

25.4 pyproject.toml:现代依赖声明

现在的标准是把项目元信息和依赖写在 pyproject.toml

toml
[project]
name = "myproject"
version = "0.1.0"
description = "一句话说明"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "ruff>=0.6",
    "mypy>=1.11",
]

[project.scripts]
mycli = "myproject.cli:main"        # 安装后可以直接敲 mycli 运行

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.ruff]
line-length = 100

[tool.pytest.ini_options]
testpaths = ["tests"]

安装:

bash
pip install -e ".[dev]"      # 安装项目本身 + dev 依赖

25.5 uv:推荐的现代工具

uv 把 Python 版本管理、虚拟环境、依赖解析、包安装统一成一个工具,速度比 pip 快 10-100 倍。

bash
# 初始化项目(自动创建 pyproject.toml)
uv init myproject
cd myproject

# 加依赖(自动创建虚拟环境、更新 pyproject.toml 和 uv.lock)
uv add httpx pydantic
uv add --dev pytest ruff mypy

# 运行(自动确保环境是最新的)
uv run python main.py
uv run pytest

# 同步环境到锁文件状态
uv sync

# 管理 Python 版本
uv python install 3.14
uv python pin 3.14

# 不安装直接运行工具
uv tool run ruff check .
uvx ruff check .              # 简写

`uv.lock` 文件记录了所有依赖的精确版本和哈希,保证在任何机器上装出完全一样的环境。这个文件要提交到 git

其他类似工具:Poetry(成熟,社区大)、PDM、Hatch。选一个坚持用就好。

25.6 依赖管理的原则

1. 区分直接依赖和锁文件

  • pyproject.toml 写你直接需要什么,版本范围宽松(httpx>=0.27
  • uv.lock / requirements.txt 记录精确版本,保证可复现

2. 版本约束别太松也别太紧

toml
"httpx"              # ❌ 太松,将来大版本更新可能直接崩
"httpx==0.27.0"      # ❌ 太紧,收不到安全修复
"httpx>=0.27,<1.0"   # ✅ 允许小版本更新,挡住破坏性变更

3. 定期更新和审计

bash
pip list --outdated
pip-audit                    # 检查已知安全漏洞

4. 谨慎添加依赖

每个依赖都是负债:可能有漏洞、可能停止维护、可能引入冲突。加之前问一下:标准库能做吗?值不值得为这一个函数引入整个库?

25.7 本章练习

练习 25.1 创建一个新目录,在其中建立虚拟环境,安装 httpx,写一个脚本获取某个 API 的数据,然后导出 requirements.txt。

练习 25.2 解释为什么 .venv/ 应该加入 .gitignore,而 uv.lock 应该提交。

练习 25.3 下面的依赖声明有什么问题?

toml
dependencies = ["django", "requests==2.25.0", "numpy>=1.0"]

<details> <summary>参考答案</summary>

25.1

bash
mkdir demo && cd demo
python3 -m venv .venv
source .venv/bin/activate
pip install httpx

cat > main.py << 'EOF'
import httpx

r = httpx.get("https://api.github.com/repos/python/cpython", timeout=10)
r.raise_for_status()
data = r.json()
print(f"{data['full_name']}: {data['stargazers_count']} stars")
EOF

python main.py
pip freeze > requirements.txt

25.2

  • .venv/ 是本地生成的产物,体积大(几十到几百 MB)、跟操作系统和 CPU 架构绑定,换台机器根本用不了。别人拿到代码自己重建即可。
  • uv.lock 记录了精确的依赖版本和哈希,提交它才能保证团队/CI/生产环境装出完全一致的环境。这是"可复现构建"的基础。

25.3 三个问题:

  • django 完全没有版本约束 —— 将来 Django 出了不兼容的大版本,项目会突然崩掉
  • requests==2.25.0 死锁到一个具体版本 —— 收不到安全补丁,还容易和其他包的依赖冲突
  • numpy>=1.0 下界太低且没有上界 —— 实际上不可能真的支持 numpy 1.0

改进:

toml
dependencies = [
    "django>=5.0,<6.0",
    "requests>=2.31,<3.0",
    "numpy>=1.26,<3.0",
]

</details>


26. 测试

26.1 为什么写测试

不是为了"证明代码是对的",而是为了:

  • 改代码时不用害怕——这是最大的价值
  • 逼你把代码写成可测试的样子(通常也就是设计更好的样子)
  • 用例本身是最准确的文档

没有测试的项目,超过某个规模后就没人敢动了。

26.2 pytest 入门

bash
pip install pytest

calculator.py

python
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为 0")
    return a / b

test_calculator.py

python
import pytest
from calculator import add, divide

def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -1) == -2

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError, match="除数不能为 0"):
        divide(10, 0)

运行:

bash
pytest                    # 运行所有测试
pytest -v                 # 显示每个测试的名字
pytest test_calc.py       # 只跑某个文件
pytest -k "divide"        # 只跑名字含 divide 的
pytest -x                 # 第一个失败就停
pytest --lf               # 只重跑上次失败的
pytest -q                 # 简洁输出

约定

  • 测试文件叫 test_*.py*_test.py
  • 测试函数叫 test_*
  • 用原生 assert,pytest 会自动展示详细的失败信息

26.3 参数化测试

同一个逻辑测多组数据,别复制粘贴:

python
@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (0.1, 0.2, pytest.approx(0.3)),      # 浮点数比较用 approx
])
def test_add(a, b, expected):
    assert add(a, b) == expected

一个函数,四个测试用例,失败时能精确定位是哪一组。

26.4 fixture:准备测试数据

python
import pytest

@pytest.fixture
def sample_users():
    return [
        {"name": "张三", "age": 25},
        {"name": "李四", "age": 17},
    ]

def test_adults(sample_users):            # 参数名匹配 fixture 名,自动注入
    adults = [u for u in sample_users if u["age"] >= 18]
    assert len(adults) == 1

带清理的 fixture

python
@pytest.fixture
def temp_db(tmp_path):                    # tmp_path 是 pytest 内置 fixture
    db_path = tmp_path / "test.db"
    conn = sqlite3.connect(db_path)
    conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    yield conn                            # 测试在这里运行
    conn.close()                          # 测试结束后清理

作用域

python
@pytest.fixture(scope="function")   # 默认:每个测试函数都新建
@pytest.fixture(scope="module")     # 每个文件建一次
@pytest.fixture(scope="session")    # 整个测试会话建一次(比如启动数据库)

常用内置 fixture

python
def test_file(tmp_path):              # 临时目录(Path 对象)
    (tmp_path / "a.txt").write_text("hi")

def test_output(capsys):              # 捕获 print 输出
    print("hello")
    assert capsys.readouterr().out == "hello\n"

def test_env(monkeypatch):            # 临时修改环境变量/属性
    monkeypatch.setenv("API_KEY", "test")
    monkeypatch.setattr("mymodule.CONFIG", {"debug": True})

conftest.py 里定义的 fixture 对整个目录的测试可见,不需要 import。

26.5 好测试的特征

1. 三段式结构(Arrange-Act-Assert)

python
def test_withdraw():
    account = BankAccount("张三", 100)      # Arrange:准备
    account.withdraw(30)                   # Act:执行
    assert account.balance == 70           # Assert:断言

2. 一个测试测一件事

python
# ❌ 失败时不知道是哪一步坏了
def test_account():
    a = BankAccount("张三", 100)
    a.deposit(50)
    assert a.balance == 150
    a.withdraw(30)
    assert a.balance == 120
    with pytest.raises(...): a.withdraw(1000)

# ✅ 拆成三个测试
def test_deposit_increases_balance(): ...
def test_withdraw_decreases_balance(): ...
def test_withdraw_too_much_raises(): ...

3. 测试名要说清楚测的是什么

python
def test_1():                                       # ❌
def test_withdraw_more_than_balance_raises():       # ✅

失败时你看到的是这个名字,它应该直接告诉你出了什么问题。

4. 测试之间相互独立

不依赖执行顺序,不共享可变状态。每个测试都能单独运行。

5. 重点测边界和异常

python
# 空输入、单元素、最大值、None、负数、超长字符串
def test_empty_list(): ...
def test_single_element(): ...
def test_none_input_raises(): ...

正常情况通常不会出错,bug 都藏在边界上。

26.6 mock:隔离外部依赖

测试不该依赖网络、数据库、时间:

python
from unittest.mock import patch, Mock

def get_user_name(user_id):
    r = httpx.get(f"https://api.example.com/users/{user_id}")
    return r.json()["name"]


def test_get_user_name():
    fake = Mock()
    fake.json.return_value = {"name": "张三"}

    with patch("mymodule.httpx.get", return_value=fake):
        assert get_user_name(1) == "张三"

⚠️ mock 用多了是设计有问题的信号。如果一个函数需要 mock 五个东西才能测,说明它做的事太多了。更好的方式是依赖注入

python
# ❌ 硬编码依赖,只能靠 mock
def get_user_name(user_id):
    r = httpx.get(...)

# ✅ 依赖作为参数传入,测试时传个假的就行
def get_user_name(user_id, client=httpx):
    r = client.get(...)

26.7 覆盖率

bash
pip install pytest-cov
pytest --cov=myproject --cov-report=term-missing
Name                 Stmts   Miss  Cover   Missing
--------------------------------------------------
myproject/core.py       45      3    93%   67-69

⚠️ 覆盖率是个有用的诊断工具,不是目标。100% 覆盖率不代表没 bug(覆盖到了不等于断言正确)。但覆盖率低的地方确实没被测到,值得看一眼。

实用做法:关注 Missing 那一列,问自己"这几行没测是有意为之吗?"

26.8 其他测试类型

doctest——文档里的例子当测试跑:

python
def add(a, b):
    """返回两数之和。

    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    """
    return a + b
bash
pytest --doctest-modules

好处是文档永远不会过时。

属性测试(hypothesis)——自动生成大量输入找反例:

python
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_is_idempotent(lst):
    assert sorted(sorted(lst)) == sorted(lst)

hypothesis 会自动尝试各种边界情况(空列表、极大值、重复元素),找到反例后还会自动"缩小"到最简的失败用例。对纯函数特别有效。

26.9 本章练习

练习 26.1 给下面的函数写测试,覆盖正常情况和所有边界:

python
def parse_age(text):
    age = int(text)
    if not 0 <= age <= 150:
        raise ValueError(f"年龄超出范围:{age}")
    return age

练习 26.2parametrize 给 FizzBuzz 函数写测试。

练习 26.3 写一个 fixture,提供一个包含三个测试文件的临时目录。

练习 26.4 下面的测试有什么问题?

python
def test_everything():
    assert add(1, 2) == 3
    assert divide(10, 2) == 5
    assert multiply(3, 4) == 12

<details> <summary>参考答案</summary>

26.1

python
import pytest
from mymodule import parse_age

@pytest.mark.parametrize("text, expected", [
    ("0", 0),
    ("25", 25),
    ("150", 150),
    (" 42 ", 42),          # int() 会忽略两端空白
])
def test_parse_age_valid(text, expected):
    assert parse_age(text) == expected


@pytest.mark.parametrize("text", ["-1", "151", "1000"])
def test_parse_age_out_of_range(text):
    with pytest.raises(ValueError, match="超出范围"):
        parse_age(text)


@pytest.mark.parametrize("text", ["abc", "", "3.14", None])
def test_parse_age_invalid_input(text):
    with pytest.raises((ValueError, TypeError)):
        parse_age(text)

26.2

python
@pytest.mark.parametrize("n, expected", [
    (1, "1"),
    (3, "Fizz"),
    (5, "Buzz"),
    (15, "FizzBuzz"),
    (30, "FizzBuzz"),
    (7, "7"),
])
def test_fizzbuzz(n, expected):
    assert fizzbuzz(n) == expected

26.3

python
@pytest.fixture
def sample_dir(tmp_path):
    (tmp_path / "a.txt").write_text("内容 A", encoding="utf-8")
    (tmp_path / "b.txt").write_text("内容 B", encoding="utf-8")
    (tmp_path / "c.log").write_text("日志", encoding="utf-8")
    return tmp_path

def test_count_txt(sample_dir):
    assert len(list(sample_dir.glob("*.txt"))) == 2

tmp_path 是 pytest 内置的,测试结束后自动清理。

26.4 三个问题:

  1. 一个测试测了三件事,第一个 assert 失败后面两个根本不会执行
  2. 测试名 test_everything 完全没有信息量,失败时不知道哪里坏了
  3. 应该拆成 test_addtest_dividetest_multiply 三个独立测试

</details>


27. 调试与日志

27.1 print 调试

最原始也最常用。别不好意思用,但用对方式:

python
print(f"{user=}")                  # user=User(name='张三')  —— f-string 的 = 语法
print(f"{len(items)=}, {items[:3]=}")

调试完记得删掉。或者——直接用日志,见 27.3。

27.2 断点调试

内置调试器 pdb

python
def process(data):
    result = transform(data)
    breakpoint()                   # Python 3.7+,程序会在这里停下
    return result

常用命令:

n (next)      执行下一行
s (step)      进入函数内部
c (continue)  继续运行到下一个断点
l (list)      显示当前代码上下文
p 变量名       打印变量
pp 变量名      美化打印
w (where)     显示调用栈
u / d         在调用栈中上下移动
q (quit)      退出

在 pdb 里可以直接执行任意 Python 表达式,非常灵活。

IDE 调试器(VS Code / PyCharm)更直观:点击行号左边设置断点,按 F5 启动,能看到所有变量、单步执行、条件断点。推荐优先用这个

出错时自动进入调试器

bash
python -m pdb -c continue script.py     # 崩溃时停在出错的地方
pytest --pdb                            # 测试失败时进入 pdb

27.3 logging:比 print 好在哪

python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)

logger = logging.getLogger(__name__)

logger.debug("详细的调试信息")
logger.info("正常的运行信息")
logger.warning("有点不对劲但还能跑")
logger.error("出错了")
logger.critical("严重错误")

比 print 好在

  • 可以按级别过滤——开发时看 DEBUG,生产只看 WARNING 以上,不用改代码
  • 自动带时间、模块名、行号
  • 可以输出到文件、按大小轮转、发到远程服务
  • 第三方库的日志也能统一管理

级别选择

级别什么时候用
DEBUG详细的诊断信息,只在排查问题时开
INFO正常的关键节点(服务启动、任务完成)
WARNING不影响运行但需要注意(用了废弃的 API、重试了一次)
ERROR某个功能失败了,但程序还能继续
CRITICAL程序无法继续

记录异常

python
try:
    risky()
except Exception:
    logger.exception("处理失败")      # 自动附带完整 traceback

logger.exception() 只能在 except 块里用,它等价于 logger.error(..., exc_info=True)

用 %s 而不是 f-string

python
logger.info("用户 %s 登录,IP %s", username, ip)     # ✅ 只在真正输出时才格式化
logger.info(f"用户 {username} 登录")                 # ⚠️ 总是格式化,DEBUG 关闭时也浪费

输出到文件 + 轮转

python
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8"
)
handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s"
))
logging.getLogger().addHandler(handler)

推荐的配置方式——在程序入口配置一次,其他模块只 getLogger(__name__)

python
# main.py
import logging

def setup_logging(verbose: bool = False):
    logging.basicConfig(
        level=logging.DEBUG if verbose else logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )
    logging.getLogger("httpx").setLevel(logging.WARNING)   # 压低吵闹的第三方库

if __name__ == "__main__":
    setup_logging()
    main()

⚠️ 库代码不要调用 `basicConfig`——那是应用的职责。库只应该 getLogger(__name__) 然后记日志。

💡 想要更漂亮的输出,试试 Richfrom rich.logging import RichHandler

27.4 常见 bug 排查思路

1. 读懂错误信息

Python 3.11+ 的错误提示已经相当聪明:

File "main.py", line 5
    print(items[i][j])
          ^^^^^^^^^^^
IndexError: list index out of range

它会用 ^^^ 精确指出是哪个表达式出的问题。

2. 最小复现

把出问题的代码剥离到最小,往往剥着剥着就发现原因了。

3. 二分法

在代码中间加个 print/断点,判断问题在前半段还是后半段,然后继续二分。

4. 检查假设

大部分 bug 源于"我以为它是 X,实际上它是 Y"。把假设明确打印出来:

python
print(f"{type(data)=}, {len(data)=}, {data[:2]=}")

5. 高频原因清单

现象常见原因
结果比预期少/多一个差一错误(range 含头不含尾,索引从 0 开始)
改了 A,B 也变了引用共享(第 8.6 节)
函数每次调用结果累加可变默认参数(第 11.2 节)
循环里的闭包全一样变量捕获(第 12.3 节)
浮点数比较不相等精度问题(第 4.5 节)
中文乱码没指定 encoding(第 6.7 节)
第二次遍历是空的迭代器已耗尽(第 19.1 节)
import 报错找不到文件名和标准库冲突(第 13.5 节)

27.5 本章练习

练习 27.1 给一个脚本配置日志:INFO 以上输出到控制台,DEBUG 以上写入文件。

练习 27.2 下面代码的日志写法有什么问题?

python
logger.debug(f"处理数据:{expensive_serialize(data)}")

练习 27.3breakpoint() 调试下面的函数,找出为什么结果不对:

python
def average(nums):
    total = 0
    for n in nums:
        total += n
    return total / len(nums) - 1

<details> <summary>参考答案</summary>

27.1

python
import logging

def setup_logging(log_file="app.log"):
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)

    fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")

    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    console.setFormatter(fmt)

    file = logging.FileHandler(log_file, encoding="utf-8")
    file.setLevel(logging.DEBUG)
    file.setFormatter(fmt)

    root.addHandler(console)
    root.addHandler(file)

关键:logger 本身设成 DEBUG(最低),由各个 handler 各自设级别过滤。

27.2 f-string 会无条件执行 expensive_serialize(data),即使日志级别是 INFO(这条 DEBUG 不会输出)也一样浪费。

python
logger.debug("处理数据:%s", data)          # 只在真要输出时才格式化
# 或者
if logger.isEnabledFor(logging.DEBUG):
    logger.debug("处理数据:%s", expensive_serialize(data))

27.3 bug 是最后多了个 - 1,运算符优先级让它变成了 (total / len(nums)) - 1

python
def average(nums):
    if not nums:
        return 0
    return sum(nums) / len(nums)

调试过程:在 return 前加 breakpoint(),用 p totalp len(nums)p total / len(nums) 逐个验证中间值,就能定位到最后一步出了问题。 </details>


28. 代码风格与工具链

28.1 PEP 8:Python 的官方风格指南

不需要全背,记住核心几条:

python
# 缩进:4 个空格
# 行长:79(PEP 8)或 88-100(现代实践,ruff 默认 88)

# 命名
variable_name = 1          # 变量、函数:snake_case
CONSTANT_VALUE = 1         # 常量:UPPER_CASE
class ClassName: ...       # 类:PascalCase
def function_name(): ...
_internal = 1              # 内部使用:前置下划线

# 空行
class A:              # 类和顶层函数之间空 2 行

    def method(self): # 方法之间空 1 行
        pass

# 空格
x = 1                 # ✅ 运算符两边加空格
x=1                   # ❌
f(a, b)               # ✅ 逗号后加空格
f(a,b)                # ❌
f(x=1)                # ✅ 关键字参数的 = 两边不加空格
d["key"]              # ✅ 括号内侧不加空格
d[ "key" ]            # ❌

# import 顺序:标准库 → 第三方 → 本项目,各组之间空一行
import os
import sys

import httpx
import pydantic

from myproject.utils import helper

别的重要约定

python
if x is None:              # ✅ 和 None 比较用 is
if not items:              # ✅ 判断空序列
if len(items) == 0:        # ❌ 啰嗦

if isinstance(x, int):     # ✅
if type(x) == int:         # ❌

try:                       # ✅ 捕获具体异常
    ...
except ValueError:
    ...
except:                    # ❌ 裸捕获
    ...

28.2 ruff:一个工具搞定格式化和检查

ruff 是用 Rust 写的 linter + formatter,速度极快,已经基本取代了 black + flake8 + isort 的组合。

bash
pip install ruff

ruff check .                # 检查问题
ruff check --fix .          # 自动修复能修的
ruff format .               # 格式化代码

配置在 pyproject.toml

toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = [
    "E", "W",    # pycodestyle
    "F",         # pyflakes(未使用的变量、导入等)
    "I",         # isort(import 排序)
    "N",         # pep8-naming
    "UP",        # pyupgrade(自动用新语法)
    "B",         # bugbear(常见 bug 模式)
    "SIM",       # simplify(简化建议)
    "RUF",       # ruff 自己的规则
]
ignore = ["E501"]        # 行长交给 formatter 管

💡 让编辑器自动格式化——VS Code 里装 Ruff 扩展,设置保存时格式化,就再也不用想代码风格了。

28.3 pre-commit:提交前自动检查

bash
pip install pre-commit

.pre-commit-config.yaml

yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9              # ← 示例版本号,请用下面的命令拉取最新
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0              # ← 同上
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
bash
pre-commit autoupdate    # 把所有 rev 更新到各仓库的最新 tag
pre-commit install       # 安装 git hook

💡 rev 必须写具体的 tag(不能写分支名),这样才能保证每个人跑的是同一个版本。写完配置先执行一次 pre-commit autoupdate,别照抄教程里的版本号。

之后每次 git commit 会自动跑检查,不通过就提交不了。这能挡住绝大部分低级问题。

28.4 写出好代码的通用建议

1. 命名是最重要的

python
d = get()                        # ❌ 什么都没说
active_users = fetch_users()     # ✅

好名字省掉大量注释。花 30 秒想一个好名字是值得的。

2. 函数要短

超过 30-40 行就该考虑拆分。一屏看不完的函数,读的人需要不停滚动,理解成本陡增。

3. 避免深层嵌套

超过 3 层缩进就该重构了。手段:提前返回、提取函数、用推导式。

4. 消除重复,但别过早抽象

同一段逻辑出现第三次时再抽象。第二次可能只是巧合。过早抽象出来的接口往往是错的,改起来比重复更痛苦。

5. 显式优于隐式

python
from mymodule import *          # ❌
from mymodule import parse      # ✅

def f(*args, **kwargs): ...     # ❌ 除非真的需要
def f(name, age): ...           # ✅

6. 让错误早暴露

python
# ❌ 静默失败,问题会在很远的地方以奇怪的形式冒出来
def get_config(key):
    return config.get(key)

# ✅ 缺配置就立刻报错
def get_config(key):
    if key not in config:
        raise KeyError(f"缺少配置项:{key}")
    return config[key]

7. 写 README

哪怕只有你一个人用。三个月后的你也是"别人"。README 至少要说清楚:这是什么、怎么装、怎么跑。

28.5 Python 之禅

在解释器里输入 import this

优美优于丑陋
明确优于隐晦
简单优于复杂
复杂优于凌乱
扁平优于嵌套
稀疏优于稠密
可读性很重要
特殊情况不足以打破规则
但实用性胜过纯粹性
错误不应悄悄溜过
除非明确要求沉默
面对歧义,拒绝猜测
应该有一种——最好只有一种——显而易见的解决方式
虽然这方式一开始可能并不显而易见,除非你是荷兰人
现在做总比不做好
但不假思索地做,不如不做
如果实现难以解释,那是个坏主意
如果实现容易解释,那可能是个好主意
命名空间是个绝妙的主意——我们应该多用它

这不是玄学,是实际的决策依据。犹豫两种写法时,想想哪个更"明确"、更"简单"、更"可读"。

28.6 本章练习

练习 28.1 按 PEP 8 重写下面的代码:

python
def calc( x,y ):
  Result=x*2+y
  if Result>10 :
    return  True
  else :
    return False

练习 28.2 下面的函数有哪些问题?重构它。

python
def p(d):
    r = []
    for i in d:
        if i['s'] == 1:
            if i['a'] > 18:
                if i['n'] != '':
                    r.append(i['n'].upper())
    return r

练习 28.3 给一个新项目配置 ruff 和 pre-commit。

<details> <summary>参考答案</summary>

28.1

python
def calc(x, y):
    return x * 2 + y > 10

除了格式,还有几个改进:

  • 变量名 Result 不该首字母大写
  • if cond: return True else: return False 直接写成 return cond
  • 但更重要的是:函数名 calc 和参数名 x, y 都没有意义,实际项目里应该起有含义的名字

28.2 问题:

  1. 函数名 p、变量名 r/d/i 完全没有信息
  2. 字典键 s/a/n 是魔法字符串,含义不明
  3. 三层嵌套 if
  4. 没有类型注解和文档

重构:

python
def get_active_adult_names(users: list[dict]) -> list[str]:
    """返回所有已激活的成年用户的名字(大写)。"""
    return [
        user["name"].upper()
        for user in users
        if user["status"] == ACTIVE and user["age"] > 18 and user["name"]
    ]

更好的做法是用 dataclass 而不是字典:

python
@dataclass
class User:
    name: str
    age: int
    status: Status

def get_active_adult_names(users: list[User]) -> list[str]:
    return [u.name.upper() for u in users
            if u.status is Status.ACTIVE and u.age > 18 and u.name]

28.3

bash
uv add --dev ruff pre-commit

pyproject.toml

toml
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "UP", "B", "SIM"]

.pre-commit-config.yaml

yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
bash
pre-commit install

</details>


29. 接下来学什么

恭喜——读到这里,你已经掌握了 Python 的核心。接下来该往哪个方向走,取决于你想做什么。

29.1 先做几个完整项目

学语法和写项目之间有一道坎,只能靠做项目跨过去。建议按难度递增:

入门级

  • 命令行待办事项管理器(文件读写 + JSON + argparse)
  • 批量文件整理工具(pathlib + 正则)
  • 密码生成器 + 强度检查(secrets + 正则)
  • 汇率/单位换算器(API 调用 + 缓存)

进阶级

  • 网页数据抓取 + 分析(httpx + Beautiful Soup + pandas)
  • 个人记账应用(SQLite + dataclass + 图表)
  • Markdown 转 HTML 的静态博客生成器
  • 自动化日报(定时任务 + 邮件/消息推送)

挑战级

  • REST API 服务(FastAPI + 数据库 + 认证)
  • 实现一个简单的解释器或编译器
  • 用 Python 实现常见算法和数据结构
  • 一个你自己每天会用的小工具

关键是"完整":包含错误处理、测试、README、能给别人用。半成品的练习价值远低于一个能跑的小工具。

29.2 按方向选择

Web 后端

  • FastAPI——现代、快、基于类型注解,新项目首选
  • Django——全家桶,适合内容型网站和后台
  • Flask——轻量灵活
  • 配套:SQLAlchemy(ORM)、Alembic(迁移)、PostgreSQL、Redis、Docker

数据分析

  • pandas / Polars(Polars 更快,正在快速普及)
  • numpy、matplotlib / plotly / seaborn
  • Jupyter Notebook
  • 配套:SQL 是必修课

机器学习 / AI

  • scikit-learn(传统机器学习)
  • PyTorch(深度学习主流)
  • Hugging Face transformers(预训练模型)
  • 调用大模型 API(anthropic、openai 的 SDK)
  • 配套:线性代数、概率统计

自动化 / 运维

  • Playwright(浏览器自动化,比 Selenium 好用)
  • paramiko / Fabric(SSH)
  • Ansible
  • schedule / APScheduler(定时任务)

桌面/图形界面

  • Textual(终端 UI,效果惊艳)
  • PySide6 / PyQt(桌面 GUI)
  • Streamlit / Gradio(快速搭数据应用界面)

游戏

  • pygame(2D)
  • Arcade

29.3 深化 Python 本身

推荐阅读

  • 官方教程和标准库文档——docs.python.org,中文版质量很好
  • 《Fluent Python》(流畅的 Python)——想真正理解 Python 的必读,第二版覆盖到 3.10
  • 《Effective Python》——90 条具体建议,实用性极强
  • Real Python——高质量教程站

值得研究的主题

  • 描述符协议、元类(写框架时才需要)
  • CPython 内部机制、性能优化
  • C 扩展 / PyO3(用 Rust 写 Python 扩展)
  • 内存模型与垃圾回收

读源码:标准库的 pathlib.pydataclasses.py 都是很好的学习材料,第三方库推荐 httpxattrs

29.4 学编程本身

语言只是工具。这些东西换任何语言都用得上:

  • Git 和版本控制——今天就该学,非常基础
  • Linux 命令行——ls/cd/grep/find/管道,效率提升巨大
  • SQL 和数据库设计——几乎所有项目都会用到
  • HTTP 和网络基础——理解请求/响应、状态码、REST
  • 算法与数据结构——不用刷题刷到疯,但要理解复杂度
  • 系统设计——如何拆分模块、如何处理并发和失败

29.5 保持进步的习惯

  1. 每天写一点,哪怕 20 分钟。持续性比强度重要。
  2. 读别人的代码。找一个你在用的小型库,把源码读一遍。
  3. 重构旧代码。三个月前写的东西现在看会很难受——这说明你进步了。改一遍。
  4. 写下来。写博客、写文档、给别人讲。能讲清楚才是真懂了。
  5. 参与开源。从改文档、修小 bug 开始。
  6. 善用 AI,但别停留在复制粘贴。让它解释为什么,而不只是给答案。

29.6 几句真心话

不用记住所有语法。 专业开发者也天天查文档。重要的是知道"有这么个东西",而不是背下用法。

卡住是常态。 每个人都会花两小时找一个拼写错误。这不代表你不适合编程,这就是编程。

代码写得丑不要紧,能跑就是胜利。 先让它工作,再让它正确,最后让它优美——顺序不要反。

做出东西比学完教程重要得多。 一个粗糙但真的在用的小工具,价值远超十个看完的教程。

祝你写得开心。


附录 A. 常见报错速查

报错典型原因怎么查
SyntaxError: invalid syntax少冒号、少括号、中文标点、用了关键字当变量名看报错行及其上一行
IndentationError缩进不一致,或混用 Tab 和空格开启编辑器"显示空白字符"
NameError: name 'x' is not defined变量拼写错、还没赋值、作用域不对检查拼写和定义位置
TypeError: unsupported operand type(s)类型不匹配,如 "3" + 5type() 检查两边类型
TypeError: 'NoneType' object is not subscriptable对 None 用了 []——通常是某个函数返回了 None检查上游函数有没有 return
TypeError: ... takes 0 positional arguments but 1 was given类方法忘了写 self加上 self
ValueError: invalid literal for int()int("abc") 这类转换失败转换前校验,或用 try/except
ValueError: too many values to unpack解包时数量不匹配检查左右两边个数
IndexError: list index out of range索引越界,常见于 range(len(x)) 的差一错误检查边界,考虑用 enumerate
KeyError: 'xxx'字典没这个键.get() 或先 in 判断
AttributeError: 'X' object has no attribute 'y'方法名拼错,或对象类型不是你以为的那个print(type(obj), dir(obj))
ModuleNotFoundError: No module named 'x'没装、虚拟环境没激活、文件名冲突pip list 确认,检查是否在虚拟环境里
ImportError: cannot import name 'x'名字不存在、循环导入检查拼写;循环导入要重构
FileNotFoundError路径错误,或工作目录不是你以为的那个print(Path.cwd()),用绝对路径
UnicodeDecodeError编码不匹配encoding="utf-8"
ZeroDivisionError除数为 0,常见于对空列表求平均除之前判断
RecursionError递归太深或缺少基线条件检查递归终止条件
UnboundLocalError函数内给全局变量赋值global/nonlocal,或改成传参
StopIteration迭代器耗尽迭代器只能用一次,需要多次就 list()
RuntimeError: dictionary changed size during iteration遍历字典时增删了键遍历 list(d.keys()) 的副本

通用排查步骤

  1. 最后一行——异常类型和消息
  2. 找 traceback 里最下面属于你的代码的那一行
  3. 在那一行前面 print(f"{变量=}") 打印相关变量
  4. 检查变量的类型是否符合你的预期
  5. 还不行就把报错信息完整复制去搜索

附录 B. 内置函数速查

python
# 类型与转换
type(x)  isinstance(x, T)  issubclass(A, B)
int()  float()  str()  bool()  bytes()  complex()
list()  tuple()  dict()  set()  frozenset()

# 数学
abs()  round()  pow(a, b, mod)  divmod(a, b)
min()  max()  sum()
bin()  oct()  hex()      # 转进制字符串
ord("A")  chr(65)         # 字符 ↔ 码点

# 序列
len()  sorted()  reversed()  enumerate()  zip()
range()  slice()
all()  any()
filter(f, it)  map(f, it)
iter()  next()

# 对象
dir(x)              # 列出所有属性
vars(x)             # 返回 __dict__
getattr(x, "name", default)
setattr(x, "name", value)
hasattr(x, "name")
delattr(x, "name")
id(x)  hash(x)
callable(x)
repr(x)  format(x, spec)

# 输入输出
print()  input()  open()

# 执行
eval("1+1")         # ⚠️ 危险,永远不要对不可信输入用
exec(code)          # ⚠️ 同上
compile()

# 其他
help(x)
globals()  locals()
super()
property()  staticmethod()  classmethod()
breakpoint()

附录 C. 语法速查卡

python
# ============ 变量与类型 ============
x = 10                      # int
y = 3.14                    # float
s = "文本"                   # str
b = True                    # bool
n = None                    # NoneType

# ============ 容器 ============
lst = [1, 2, 3]             # list   有序可变
tup = (1, 2, 3)             # tuple  有序不可变
dct = {"k": "v"}            # dict   键值映射
st  = {1, 2, 3}             # set    无序去重

# ============ 字符串 ============
f"{name} 今年 {age} 岁"
f"{pi:.2f}"  f"{n:,}"  f"{r:.1%}"  f"{x=}"
s.strip().lower().split(",")
",".join(items)
s.replace(old, new)
s.startswith(p)  s.endswith(p)
s[::-1]                     # 反转
r"原始\字符串"

# ============ 切片 ============
s[start:stop:step]          # 含头不含尾
s[:3]  s[3:]  s[::-1]  s[-3:]

# ============ 流程控制 ============
if cond:
    ...
elif other:
    ...
else:
    ...

x = a if cond else b        # 三元

for item in iterable:
    if skip: continue
    if stop: break
else:                       # 没 break 才执行
    ...

while cond:
    ...

match value:
    case pattern: ...
    case _: ...

# ============ 循环工具 ============
range(start, stop, step)
enumerate(items, start=1)
zip(a, b, strict=True)
reversed(items)  sorted(items, key=f, reverse=True)

# ============ 推导式 ============
[f(x) for x in it if cond]          # 列表
{k: v for k, v in it}               # 字典
{f(x) for x in it}                  # 集合
(f(x) for x in it)                  # 生成器

# ============ 函数 ============
def f(a, b=1, *args, key=None, **kwargs) -> int:
    """文档字符串。"""
    return a + b

lambda x: x * 2

f(*list_args, **dict_kwargs)        # 解包传参

# ============ 类 ============
class Child(Parent):
    class_attr = 0

    def __init__(self, x):
        super().__init__()
        self.x = x

    def method(self): ...

    @property
    def value(self): return self._v

    @classmethod
    def create(cls): return cls()

    @staticmethod
    def helper(): ...

@dataclass
class Point:
    x: float
    y: float = 0.0

# ============ 异常 ============
try:
    ...
except (ValueError, TypeError) as e:
    ...
except Exception:
    raise
else:
    ...
finally:
    ...

raise ValueError(f"具体信息:{value}")
raise NewError("...") from original

with open(path, encoding="utf-8") as f:
    ...

# ============ 生成器 ============
def gen():
    yield 1
    yield from other_iterable

# ============ 装饰器 ============
import functools

def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@deco
def f(): ...

# ============ 类型注解 ============
def f(a: int, b: str = "") -> list[int]: ...
x: str | None = None
d: dict[str, list[int]] = {}

# ============ 文件 ============
from pathlib import Path

p = Path("dir") / "file.txt"
p.read_text(encoding="utf-8")
p.write_text(text, encoding="utf-8")
p.exists()  p.is_file()  p.name  p.stem  p.suffix  p.parent
list(p.parent.glob("*.txt"))
list(Path(".").rglob("*.py"))

# ============ 常用导入 ============
import json, re, os, sys, math, random, time
from pathlib import Path
from datetime import datetime, date, timedelta
from collections import Counter, defaultdict, deque
from dataclasses import dataclass, field
from functools import cache, wraps, partial
from itertools import chain, islice, groupby, product
from enum import Enum, auto
from typing import Any, Protocol, TypeVar

# ============ 脚本入口 ============
def main() -> None:
    ...

if __name__ == "__main__":
    main()

本指南到此结束。祝你在 Python 的世界里玩得开心。