编程语言研究书库

enum、集合与异常 — 把「拼错」变成「报错」

enum、集合与异常 — 把「拼错」变成「报错」

这一章讲三样「防守型」工具:enum、set、异常。 它们的共同点,是原书第 05 章那条「字符串反模式」的正面解法—— 当程序里有一个「只能取某几个值」的选项时,别用字符串,用 enum,拼错一个字母都会立刻报错; 当程序进入「不该继续」的状态时,别返回一个特殊值,抛异常,让错误带着位置和信息炸出来。

1. 这一章讲什么

前两章立了规矩:字符串只存文本、dict 按名字存取。本章解决两个「剩下的常见错误源」:

  • 选项值:颜色只能取红绿蓝,代码里写 "blue",手滑写成 "blu"——程序不报错,结果悄悄错;1
  • 出错状态:除以零、文件不存在——程序该怎么办?闷头返回一个奇怪的值,还是大声喊出来?2
  防守工具      防什么                         代价
  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 —— 要背后的数值时再取

然后看防守效果——故意拼错:

favoritecolor = color.BLU
# AttributeError: BLU. Did you mean: 'BLUE'?

报错指名道姓地告诉你错在哪、可能想写什么。对比字符串版本:"blu" 不报错, 程序带着一个永远匹配不上的值继续跑,「后果完全不可预测」。1

为什么有效、边界在哪: enum 把「值的集合」从约定变成代码里的实物——点名册在,点名就能发现缺勤。 边界只有一个,书里点明了:所有可能的值必须事先都知道;运行时才动态确定的选项集合,enum 帮不上,那该是 dict 的场景。3

作者的态度写在一个小框里:「enum 很棒,但很多程序员不知道它。凡是预先定好的一组取值,都用 enum,别用字符串。」4

3. 集合(set):自动去重的袋子

解决什么问题: 一组东西里不能有重复,或者你只想快速判断「在不在」。

怎么做: 集合的字面量是大括号({1, 2, 3}——和 dict 的区别是里面没有冒号):

a = {1, 2, 3}
a.add(1)            # 再加一次 1 —— 没用,a 还是 {1, 2, 3}
len(a)              # 3
1 in a              # True —— 成员判断,set 的看家本领

条目唯一、无顺序、不能按下标取——这三条都是同一性质(背后是哈希)的不同侧面。 书里给了它的高光用法:一行判断列表元素是否全不相同:

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,字面:举起一个异常对象,程序沿调用链向上传,直到有人接住):

1/0
# ZeroDivisionError: float division by zero

注意一个对照:有的语言对除以零返回 inf(无穷)或 NaN(Not a Number,非数); Python 选择抛异常——「尽量精确地告诉你出了什么错、在哪一行」。2

自定义场景同理。书里例子:用户输入薪水,负数薪水没有意义,应该当场拦下:

def form(salary):
    if salary < 0:
        raise Exception("Salary must not be negative")

为什么有效: 异常把「错误」从返回值里分离出来——返回值继续表达正常结果, 错误走一条独立的、不会混的通道,还带着调用栈位置。

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

块什么时候执行典型用途
excepttry 里抛了异常处理错误、告知用户
elsetry 里没抛异常「一切顺利才做」的后续动作
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 造个新类型:

class MyException(Exception):
    pass

然后可以 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

  1. 预定义选项用 enum:把拼错从「悄悄错」变成「当场报错」;代价是值必须事先列全。
  2. set 管去重和「在不在」;一行判重:len(set(xs)) == len(xs)。
  3. 出错抛异常,别返回怪值;Python 对除零选择抛异常而不是返回 NaN。
  4. try-except 接异常,else 管「顺利时」,finally 管「无论如何收尾」;接在能处理它的地方。
  5. 异常不用于常规流程;预期会发生的,先检查。
  6. 自定义异常 = 继承 Exception;用它区分「用户的错」和「开发者的错」。
  7. Python 签名上看不出会抛什么——这是动态语言的代价,靠文档和测试补(第 15 章)。

7A. 三样工具的同一张底牌

enum、set、异常各不相同,但底牌是同一张:让「不对劲」在最早的时刻、以最大的声音出现。

工具不对劲指出现的时刻
enum选项拼错/不存在代码运行到那一行
set重复/成员判断含糊结构层面(无重复是承诺)
异常状态不该继续出错现场,带调用栈

三者都不增加「正常运行」的能力——它们增加的是出错时定位的速度;而定位速度,就是调试成本的大头——bug 越早炸,修复越便宜,这是软件工程里少数没有例外的规律之一;enum、set、异常,都是这条规律的低成本实现,值得按需取用——也值得一读再读。 而定位速度,在作者的账本里,是软件成本的大头:bug 越早炸,修复越便宜; 这条规律有个经典名字(左移,书外),三样工具都是它的低成本实现。

8. 原文地图

主题原书章原文位置
字符串选项的拼写风险Enumstext/17-fm-enums.txt:10(搜「blu」)
enum 定义与 value 访问Enumstext/17-fm-enums.txt:16(搜「class Color」) · text/17-fm-enums.txt:24(搜「value」)
拼错报 AttributeErrorEnumstext/17-fm-enums.txt:30(搜「Did you mean」)
「凡预定义取值都用 enum」Enumstext/17-fm-enums.txt:39(搜「predefined selection」)
set 行为与去重一行Rarely Used Data Structurestext/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、ZeroDivisionErrorRarely Used Data Structurestext/18-fm-rarely-used-data-structures.txt:44(搜「float division by zero」)
自定义 raise(薪水例子)Rarely Used Data Structurestext/18-fm-rarely-used-data-structures.txt:58(搜「Salary must not be negative」)
try/except/else/finallyRarely Used Data Structurestext/18-fm-rarely-used-data-structures.txt:66(搜「Try, Catch, Else, Finally」)
何时用异常、在何处接Rarely Used Data Structurestext/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 Structurestext/18-fm-rarely-used-data-structures.txt:148(搜「checked exceptions」)
自定义异常与「谁犯了错」分流Rarely Used Data Structurestext/18-fm-rarely-used-data-structures.txt:158(搜「MyException」) · text/19-fm-code-structure.txt:22(搜「deliberately thrown」)

Footnotes

  1. 出处:「Enums」第 3–13 段(text/17-fm-enums.txt:10,搜「blu」;「完全不可预测的后果」见第 11 段,搜「unpredictable」)。 ↩ ↩2 ↩3 ↩4 ↩5

  2. 出处:「Rarely Used Data Structures」第 37–50 段(text/18-fm-rarely-used-data-structures.txt:44,搜「float division by zero」)。 ↩ ↩2

  3. 出处:「Enums」第 14–35 段(text/17-fm-enums.txt:16,搜「class Color」;「必须事先知道所有值」见第 35 段,搜「in advance」)。 ↩ ↩2

  4. 出处:「Enums」第 37–40 段(「About Enums」框,text/17-fm-enums.txt:39,搜「predefined selection」)。 ↩

  5. 出处:「Rarely Used Data Structures」第 8–33 段(text/18-fm-rarely-used-data-structures.txt:10,搜「unique」;第 27 段,搜「has_all_unique_elements」)。 ↩ ↩2

  6. 出处:「Rarely Used Data Structures」第 66–104 段(text/18-fm-rarely-used-data-structures.txt:82,搜「else block」;第 95 段,搜「regardless」)。 ↩

  7. 出处:「Rarely Used Data Structures」第 108–140 段(text/18-fm-rarely-used-data-structures.txt:109,搜「control flow」;第 117 段,搜「where you can deal with it」)。 ↩ ↩2 ↩3

  8. 出处:「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」)。 ↩ ↩2

  9. 出处:「Rarely Used Data Structures」第 142–152 段(text/18-fm-rarely-used-data-structures.txt:148,搜「checked exceptions」)。 ↩

  10. 出处:「14 Some Examples」第 20–27 段(text/30-ch14-14-some-examples.txt:23,搜「class Field」)。 ↩

On this page