enum、集合与异常 — 把「拼错」变成「报错」¶
这一章讲三样「防守型」工具:enum、set、异常。 它们的共同点,是原书第 05 章那条「字符串反模式」的正面解法—— 当程序里有一个「只能取某几个值」的选项时,别用字符串,用 enum,拼错一个字母都会立刻报错; 当程序进入「不该继续」的状态时,别返回一个特殊值,抛异常,让错误带着位置和信息炸出来。
1. 这一章讲什么¶
前两章立了规矩:字符串只存文本、dict 按名字存取。本章解决两个「剩下的常见错误源」:
防守工具 防什么 代价
enum 选项拼错(静默错 → 立刻报错) 得事先列出所有可能值
set 重复条目、成员判断 没有顺序、不能按下标取
异常 「不该继续」的状态被忽略 函数签名上看不出会抛什么
图说:三样工具都遵循同一哲学——让错误尽早、尽量大声地暴露。
2. enum:选项的「点名册」¶
解决什么问题: 一个变量只能取预先定好的一组值(颜色、状态、类型),用字符串写,拼错无法被发现。1
怎么做(主线例子): 定义一个 enum(enumeration,枚举——把允许的值一一列出):3
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
favoritecolor = Color.BLUE # 用 Color.BLUE,不用 "blue"
favoritecolor.value # 3 —— 要背后的数值时再取
然后看防守效果——故意拼错:
报错指名道姓地告诉你错在哪、可能想写什么。对比字符串版本:"blu" 不报错,
程序带着一个永远匹配不上的值继续跑,「后果完全不可预测」。1
为什么有效、边界在哪: enum 把「值的集合」从约定变成代码里的实物——点名册在,点名就能发现缺勤。 边界只有一个,书里点明了:所有可能的值必须事先都知道;运行时才动态确定的选项集合,enum 帮不上,那该是 dict 的场景。3
作者的态度写在一个小框里:「enum 很棒,但很多程序员不知道它。凡是预先定好的一组取值,都用 enum,别用字符串。」4
3. 集合(set):自动去重的袋子¶
解决什么问题: 一组东西里不能有重复,或者你只想快速判断「在不在」。
怎么做: 集合的字面量是大括号({1, 2, 3}——和 dict 的区别是里面没有冒号):
条目唯一、无顺序、不能按下标取——这三条都是同一性质(背后是哈希)的不同侧面。 书里给了它的高光用法:一行判断列表元素是否全不相同:
def has_all_unique_elements(mylist):
return len(set(mylist)) == len(mylist)
has_all_unique_elements([1, 2, 3]) # True
has_all_unique_elements([1, 2, 3, 1]) # False
思路:set 会吃掉重复,所以「set 化之后长度不变 ⟺ 原本没有重复」。5
边界: 作者自陈「不常用 set,但有用」——因为它的场景窄:去重和成员判断。 日常数据的首选仍是 list 和 dict;set 是「需要 uniqueness 或快速 membership 时」的特化工具。
4. 异常:程序的大声求救¶
4.1 抛出:出错就喊,别憋着¶
解决什么问题: 程序进入了一个无法按数学/业务定义继续的状态(除以零、文件不存在), 继续下去只会产生「不可预见的后果」。
怎么做: Python 的做法是抛异常(raise an exception,字面:举起一个异常对象,程序沿调用链向上传,直到有人接住):
注意一个对照:有的语言对除以零返回 inf(无穷)或 NaN(Not a Number,非数);
Python 选择抛异常——「尽量精确地告诉你出了什么错、在哪一行」。2
自定义场景同理。书里例子:用户输入薪水,负数薪水没有意义,应该当场拦下:
为什么有效: 异常把「错误」从返回值里分离出来——返回值继续表达正常结果, 错误走一条独立的、不会混的通道,还带着调用栈位置。
4.2 接住:try / except / else / finally¶
解决什么问题: 抛出的异常若没人接,整个程序崩溃。多数时候你想优雅处理:提示用户、记日志、继续或体面退出。
怎么做: try-except 块——把可能出事的代码放进 try,把「出事之后的动作」放进 except:
try:
risky_function()
except Exception as e:
print(f"An error occurred: {e}") # 接住异常,程序不崩
else:
print("Everything went fine!") # 只有没抛异常才执行
finally:
... # 无论抛没抛都执行:收尾的地方(关文件等)
三个附属块的分工,书里讲得很清:6
| 块 | 什么时候执行 | 典型用途 |
|---|---|---|
except |
try 里抛了异常 | 处理错误、告知用户 |
else |
try 里没抛异常 | 「一切顺利才做」的后续动作 |
finally |
无论抛没抛 | 清理:关文件、释放资源 |
可以接多个 except,按类型分流——except ZeroDivisionError 接除零、except ValueError 接类型转换失败,
先具体后宽泛(宽泛的 Exception 兜底放最后)。书里的完整例子是一个除法程序:
div 函数只管抛,main 函数负责接——「在哪能处理,就在哪接」是选接的位置的经验法则。7
还可以再进一层:把整个 main() 包进一个 try-except,用「自定义异常 vs 通用 Exception」区分谁犯了错——
抛自定义异常的,是「用户做错了」(改输入再来);抛通用异常的,是「开发者漏想了某种情况」(请联系支持)。
这个区分思想在第 15 章测试里还会回来。8
4.3 什么时候用异常,什么时候不用¶
书里的边界划得很清:
- 用: 处理正常运行中不该出现的错误。「文件不存在」对
open来说就是该用异常的场景(虽然可以先检查,但 Python 社区惯例是直接 open 接异常)。 - 不用: 拿异常当流程控制——「预期会常规发生」的情况,先查再问(如
os.path.exists),别用 try-except 当 if 用。7
4.4 异常的代价:签名上看不见¶
作者把异常的短板说得很坦白:函数签名上看不出它会抛什么异常——想知道,只能读文档(若有)或通读实现。 Java 曾用检查式异常(checked exceptions)解决:编译器强制你接住函数可能抛的一切。 Python 不打算学这招,理由:它让代码更复杂、更难读;Python 把「读文档、搞懂会抛什么」留给程序员自觉。9
自定义异常的做法极简——继承 Exception 造个新类型:
然后可以 except MyException 精确分流。类型即分类:异常类描述「哪一类问题」,异常对象里的文本是「具体的描述」。8
5. 作者的判断与证据¶
书里给了证据的: enum 拼错报错的具体报错信息;set 去重示例;ZeroDivisionError 的实际输出;多 except 分流的完整程序。157
作者的判断:
- 「别用异常做流程控制」——行业主流共识(Python 社区的 EAFP:宁求原谅不求许可,即先试再补,正是建立在异常便宜的前提上;书里没提这个缩写,补充);
- 「Python 不引检查式异常是因为会复杂化」——这是对语言设计的归因,与 Python 官方立场一致;
- 「enum 优于字符串」——他的高频主张,第 19 章的渗流模拟会再用一次(棋盘四态用 enum 定义)。10
判断(我们的,不是书里的): 本章三工具的共通哲学——把「约定的正确」升级为「机制保证的正确」—— 是静态语言用类型系统做的事,Python 用运行时机制(enum 查属性、异常中断控制流)补位。 代价是保障都在运行时:enum 拼错要运行到那行才发现,继承 Exception 写错不报错。 这正好呼应第 01 章「测试替代编译器」——工具链越动态,测试网就要越密。 如果错,会错在: 大型 Python 项目如今靠类型标注 + mypy 把大量检查挪回写代码时; 如果类型工具普及到入门教材,这套「运行时防守」会被描述成「第二道防线」而非主力——但防线本身仍然必要。
6. 边界与局限¶
- 异常部分没讲 异常的继承体系(Exception 之下还有 ValueError、TypeError 等一大族)和
raise ... from链; - 没讲
with语句本身就是异常安全的(第 14 章的with open是上下文管理器,finally 的现代化身); - set 的运算(交并差)没提——只给了成员判断和去重;
- enum 的高级功能(IntEnum、自动取值
auto())没提,日常用不到。
7. 可带走的¶
本章主线例子,一行写完: 颜色选项用 Color.BLUE 不用 "blue" → 拼成 BLU 立刻报
AttributeError: BLU. Did you mean: 'BLUE'?;而字符串 "blu" 会静默出错。1
- 预定义选项用 enum:把拼错从「悄悄错」变成「当场报错」;代价是值必须事先列全。
- set 管去重和「在不在」;一行判重:
len(set(xs)) == len(xs)。 - 出错抛异常,别返回怪值;Python 对除零选择抛异常而不是返回 NaN。
- try-except 接异常,else 管「顺利时」,finally 管「无论如何收尾」;接在能处理它的地方。
- 异常不用于常规流程;预期会发生的,先检查。
- 自定义异常 = 继承 Exception;用它区分「用户的错」和「开发者的错」。
- Python 签名上看不出会抛什么——这是动态语言的代价,靠文档和测试补(第 15 章)。
7A. 三样工具的同一张底牌¶
enum、set、异常各不相同,但底牌是同一张:让「不对劲」在最早的时刻、以最大的声音出现。
| 工具 | 不对劲指 | 出现的时刻 |
|---|---|---|
| enum | 选项拼错/不存在 | 代码运行到那一行 |
| set | 重复/成员判断含糊 | 结构层面(无重复是承诺) |
| 异常 | 状态不该继续 | 出错现场,带调用栈 |
三者都不增加「正常运行」的能力——它们增加的是出错时定位的速度;而定位速度,就是调试成本的大头——bug 越早炸,修复越便宜,这是软件工程里少数没有例外的规律之一;enum、set、异常,都是这条规律的低成本实现,值得按需取用——也值得一读再读。 而定位速度,在作者的账本里,是软件成本的大头:bug 越早炸,修复越便宜; 这条规律有个经典名字(左移,书外),三样工具都是它的低成本实现。
8. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 字符串选项的拼写风险 | Enums | text/17-fm-enums.txt:10(搜「blu」) |
| enum 定义与 value 访问 | Enums | text/17-fm-enums.txt:16(搜「class Color」) · text/17-fm-enums.txt:24(搜「value」) |
| 拼错报 AttributeError | Enums | text/17-fm-enums.txt:30(搜「Did you mean」) |
| 「凡预定义取值都用 enum」 | Enums | text/17-fm-enums.txt:39(搜「predefined selection」) |
| set 行为与去重一行 | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:10(搜「entries are unique」) · text/18-fm-rarely-used-data-structures.txt:27(搜「has_all_unique_elements」) |
| 异常 vs inf/NaN、ZeroDivisionError | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:44(搜「float division by zero」) |
| 自定义 raise(薪水例子) | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:58(搜「Salary must not be negative」) |
| try/except/else/finally | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:66(搜「Try, Catch, Else, Finally」) |
| 何时用异常、在何处接 | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:108(搜「not expected during normal operation」) · text/18-fm-rarely-used-data-structures.txt:117(搜「where you can deal with it」) |
| 检查式异常之贬 | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:148(搜「checked exceptions」) |
| 自定义异常与「谁犯了错」分流 | Rarely Used Data Structures | text/18-fm-rarely-used-data-structures.txt:158(搜「MyException」) · text/19-fm-code-structure.txt:22(搜「deliberately thrown」) |
-
出处:「Enums」第 3–13 段(
text/17-fm-enums.txt:10,搜「blu」;「完全不可预测的后果」见第 11 段,搜「unpredictable」)。 ↩↩↩↩↩ -
出处:「Rarely Used Data Structures」第 37–50 段(
text/18-fm-rarely-used-data-structures.txt:44,搜「float division by zero」)。 ↩↩ -
出处:「Enums」第 14–35 段(
text/17-fm-enums.txt:16,搜「class Color」;「必须事先知道所有值」见第 35 段,搜「in advance」)。 ↩↩ -
出处:「Enums」第 37–40 段(「About Enums」框,
text/17-fm-enums.txt:39,搜「predefined selection」)。 ↩ -
出处:「Rarely Used Data Structures」第 8–33 段(
text/18-fm-rarely-used-data-structures.txt:10,搜「unique」;第 27 段,搜「has_all_unique_elements」)。 ↩↩ -
出处:「Rarely Used Data Structures」第 66–104 段(
text/18-fm-rarely-used-data-structures.txt:82,搜「else block」;第 95 段,搜「regardless」)。 ↩ -
出处:「Rarely Used Data Structures」第 108–140 段(
text/18-fm-rarely-used-data-structures.txt:109,搜「control flow」;第 117 段,搜「where you can deal with it」)。 ↩↩↩ -
出处:「Rarely Used Data Structures」第 156–170 段(
text/18-fm-rarely-used-data-structures.txt:158,搜「inherit from the class exception」)与「Code Structure」第 11–24 段(text/19-fm-code-structure.txt:22,搜「deliberately thrown」)。 ↩↩ -
出处:「Rarely Used Data Structures」第 142–152 段(
text/18-fm-rarely-used-data-structures.txt:148,搜「checked exceptions」)。 ↩ -
出处:「14 Some Examples」第 20–27 段(
text/30-ch14-14-some-examples.txt:23,搜「class Field」)。 ↩