第 04 章
异常:为出错的那一天提前写代码
CS50P 第 3 周:区分语法错误与运行时错误,用 try / except 捕获 ValueError,理解 else 与 try 的最小区间,再借 while True 与自定义 get_int 把「反复追问直到输入正确」封装成可复用函数。
Exceptions: Writing Code for the Day Things Go Wrong
CS50P week 3: separate syntax errors from runtime errors, catch ValueError with try / except, understand else and the smallest possible try block, then wrap repeated input validation in a reusable get_int function.
篇内目录
- What this chapter covers
- Timeline
- Two kinds of error
- Syntax errors
- Runtime errors
- try / except
- Why not a bare except
- Keep the try block small
- A subtle trap: NameError
- Asking again in a loop
- Packaging get_int
- pass: catch and say nothing
- Two styles: EAFP and LBYL
- Chapter summary
- Comprehension check
- Relation to neighbouring chapters
这一章在讲什么
这是长视频的第 4 章,对应 CS50P 第 3 周(04:07:34–04:52:00),主题是异常。
「异常」在日常英语里是「例外、特别」,在编程里恰恰相反:它指出了错。上一章末尾已经埋下伏笔——while True 要求用户输入正数,可如果用户输入的是 cat 呢?int("cat") 会直接崩溃,而这个崩溃发生在你写完代码很久之后,由别人触发。
本章先区分两类错误:写错的语法错误,和运行时才出现的运行时错误。然后引入 try / except,把「可能失败的代码」与「失败后的处理」分开;接着补上 else,讲清楚 try 的范围应当尽量小;最后把整套输入校验封装成 get_int(prompt),并讨论 pass 与「先试再说」的 Python 风格。
时间轴
| 时间 | 内容 |
|---|---|
| 4:07:34 | 本周主题:什么算异常 |
| 4:08:05 | 语法错误:未闭合的字符串字面量 |
| 4:09:28 | 语法错误必须自己修,无法被捕获 |
| 4:10:36 | number.py:int(input(...)) 的隐患 |
| 4:13:27 | 边界测试:0、负数、以及 cat |
| 4:14:09 | ValueError:invalid literal for int() |
| 4:16:05 | try / except ValueError |
| 4:19:00 | 能不能什么都不写、捕获所有错误?(不推荐) |
| 4:20:29 | try 的范围应当只包住会出错的那一行 |
| 4:21:38 | 移动 print 之后出现的 NameError |
| 4:25:52 | try / except / else:成功才继续 |
| 4:29:41 | 用 while True 反复追问,而不是直接退出 |
| 4:33:40 | break 的位置与 return 的关系 |
| 4:36:44 | 两种写法都合理:可读性 vs 最小尝试范围 |
| 4:37:07 | 封装 get_int() |
| 4:40:39 | 用 return 代替 break 再 return |
| 4:42:48 | pass:捕获但静默忽略 |
| 4:48:47 | 给 get_int 加 prompt 参数 |
| 4:51:08 | raise 预告 |
两类错误
语法错误
print("hello, world)
SyntaxError: unterminated string literal
unterminated 是「没有终止」,literal 是「你直接写出来的字面量」。翻译过来就是:你开了一个字符串,却没把它关上。
语法错误的特点是:程序根本不会运行。它不是程序运行中出的问题,而是代码本身不合法。因此它无法被 try / except 捕获——解释器在解析阶段就已经拒绝了这个文件。
运行时错误
x = int(input("What's x? "))
print(f"x is {x}")
输入 50、0、-1 都没问题。但输入 cat:
ValueError: invalid literal for int() with base 10: 'cat'
ValueError 的意思是:int 收到了一个值,但这个值不合法。程序写得很对,问题出在用户——而用户的行为你无法事先知道。
这类错误发生在运行时,因此可以在代码里提前布置处理逻辑。
一句话总结: 语法错误是「你写错了」,运行时错误是「世界没按你预想的方式运行」。
try / except
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
执行顺序:
- 尝试执行
try块; - 如果没有异常,跳过
except,执行else; - 如果抛出
ValueError,跳过try块剩余部分,执行except块,else不执行。
except ValueError 里的类型名必须写对,大小写敏感。
为什么不要写裸 except
Python 允许只写 except:,捕获一切。但课程明确反对:
- 你不知道到底出了什么错,就不知道该怎么处理;
- 它会把真正的 bug 一起吞掉,程序悄悄出错却继续运行。
好的做法是明确列出可能发生的错误类型。
try 的范围要小
try:
x = int(input("What's x? "))
print(f"x is {x}")
except ValueError:
print("x is not an integer")
这段能跑,但 print 也被放进了 try。print 几乎不可能抛 ValueError,把它包进去只会让「哪一步可能失败」变得模糊。推荐:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
一个隐蔽的坑:NameError
如果把 print 移出 try,但写成:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
print(f"x is {x}")
输入 cat 时,输出不是友好的提示,而是:
NameError: name 'x' is not defined
原因在于赋值是最后一步。执行 x = int(input(...)) 时,Python 先算右边:input 拿到 "cat",int("cat") 抛出 ValueError。异常打断执行,右边的值从未产生,于是左边那一步赋值根本没发生,x 从未被创建。
except 只是把错误接住了,并不会给 x 一个值。之后那行 print 访问一个不存在的名字,于是又抛出一个新错误。
else 正是为了解决这个问题:只有 try 成功时才执行。
用循环反复追问
上面的程序遇到错误就结束,对用户不友好。更合理的做法是继续问:
while True:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
break
print(f"x is {x}")
现在输入 cat 会提示并重新询问,直到输入合法整数为止。
注意 break 放在 else 里:else 与 try 绑定,不是与 except 绑定——它只在没有异常时执行。
也可以不用 else,把 break 直接放在 try 块末尾:
while True:
try:
x = int(input("What's x? "))
break
except ValueError:
print("x is not an integer")
两种写法都正确。课程的态度是:只要能说清理由,都算好设计;差别只在「最小尝试范围」与「少一层嵌套」之间取舍。
封装成 get_int
一旦发现自己在反复写同一段输入校验,就该把它抽成函数:
def main():
x = get_int("What's x? ")
print(f"x is {x}")
def get_int(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
pass
main()
几个演进步骤值得注意:
- 先写清楚:
x = int(input(prompt)),成功则break,出了循环再return x。 - 合并:
break之后紧接着就是return x,直接用return x更简洁——return既能跳出循环,又能交出值。 - 再压缩:
x只被用一次,可以省掉变量,直接return int(input(prompt))。 - 加参数:把提示语交给调用者决定,
get_int不假设自己一定在问 x。这让它既能问x,也能问y或age。
最终 main 只剩两行:取数、打印。取数的复杂度被封装在函数内部。
pass:捕获但什么都不做
except 块里写 pass,表示「捕获到异常,但不再做任何处理」。程序不会崩溃,也不会打印错误,而是继续循环、重新提示。
while True:
try:
return int(input(prompt))
except ValueError:
pass
效果是用户只会看到提示语一次次重复。这算不算更好,取决于场景:有人觉得安静更友好,有人觉得不说明原因反而困惑。
两种风格:EAFP 与 LBYL
Python 社区偏爱 EAFP(Easier to Ask Forgiveness than Permission):先做,出错再处理。
try:
x = int(input("What's x? "))
except ValueError:
...
其他语言常见 LBYL(Look Before You Leap):先检查,再行动。
s = input("What's x? ")
if s.isnumeric():
x = int(s)
else:
...
两种都合理。课程的说法是:Python 的惯性偏向 EAFP,但先检查也完全可行,关键是知道自己在选哪一种。
本章结论
- 语法错误无法被捕获,必须改代码;运行时错误可以在代码里处理。
ValueError表示「值不合法」,int("cat")就会触发。try/except把可能失败的代码与失败后的处理分开。- 不要写裸
except:;明确列出错误类型。 try只包住真正可能出错的行。- 赋值是最后一步:右边出错,左边就没有值,后续访问会得到
NameError。 else只在try成功时执行,与try绑定。while True+try/except/break是输入校验的标准结构。return既能跳出循环又能返回值,比break再return简洁。pass表示捕获后静默忽略。- 把「取一个合法整数」封装成
get_int(prompt),调用方只关心结果。
理解检验
1. 下面这段代码输入 cat 时会怎样?为什么?怎么修?
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
print(f"x is {x}")
参考答案:会抛出 NameError: name 'x' is not defined。因为 x = int(input(...)) 的执行顺序是先计算右边:int("cat") 抛出 ValueError,异常打断了这一行,右边的值没有产生,赋值从未发生,x 也就从未被创建。except 只是接住错误,并没有给 x 任何值。
修法是用 else,让打印只在成功时执行:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
2. else 是与 try 绑定还是与 except 绑定?它什么时候执行?
参考答案:与 try 绑定。它只在 try 块没有抛出任何异常时执行。若 try 抛出被捕获的异常,执行 except 块,else 被跳过;若抛出未被捕获的异常,程序直接向上传播,else 同样不执行。
3. 为什么不建议写裸 except:?
参考答案:裸 except: 会捕获所有异常,包括你根本没预料到的类型。这样做有两个问题:一是你不知道实际发生了什么,也就无法正确处理;二是它会把真正的 bug 一并吞掉,程序带着错误继续运行,问题被掩盖。正确做法是明确写出预期会出现的异常类型,例如 except ValueError。
4. 把 print(f"x is {x}") 放进 try 块有什么不好?
参考答案:print 几乎不可能抛 ValueError,把它放进 try 会扩大「可能失败」的范围,让读者难以判断哪一行才是真正需要保护的。try 应当只包住会抛异常的最小代码,通常就是 int(input(...)) 这一行。这样错误处理的边界清晰,也避免误捕获其他代码抛出的同类异常。
5. 把 number.py 改成 while True 版本后,为什么输入 cat 不会退出程序?
参考答案:因为 except ValueError 接住了异常,try 失败后程序继续执行到循环末尾,while True 的条件仍为真,于是回到循环开头再次询问。只有当 try 成功、进入 else 执行 break 时,循环才会结束。异常被捕获后不再传播,所以程序不会崩溃退出。
6. 把 get_int 写成下面这样,有什么可以改进的地方?
def get_int():
while True:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
break
return x
参考答案:可以合并 break 与 return——else 里直接 return x,就不需要跳出循环后再返回:
def get_int(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("x is not an integer")
return 会同时结束循环并交回值。另外提示语被写死在函数里,只能问 x;加一个 prompt 参数后,函数可以被复用来问任意变量。若不想每次都打印提示,也可以把 except 块改成 pass。
与前后章的关系
异常处理把上一章的输入校验从「只要用户配合」变成「用户不配合也能继续」,是写出健壮程序的基础。下一章讲库,会引入大量可能抛异常的外部函数,本章的 try / except 正是应对它们的手段。
get_int(prompt) 这样的封装还会在文件读写、单元测试里反复出现:把「可能失败的操作 + 重试」包起来,调用方只处理成功的结果。
What this chapter covers
This is chapter 4 of the long video, covering CS50P week 3 (04:07:34–04:52:00), on exceptions.
In everyday English an "exception" is something special or rare; in programming it is the opposite — something has gone wrong. The previous chapter left a loose end: while True insists that the user enter a positive number, but what if the user types cat? int("cat") crashes, and it crashes long after you wrote the code, triggered by someone else.
The chapter first separates two kinds of error: syntax errors you made while typing, and runtime errors that only appear while running. It then introduces try / except to separate "code that might fail" from "what to do when it does", adds else, argues for keeping the try block as small as possible, and finally wraps the whole pattern in get_int(prompt). It closes with pass and Python's "try it first" style.
Timeline
| Time | Topic |
|---|---|
| 4:07:34 | The week's theme: what counts as an exception |
| 4:08:05 | Syntax error: an unterminated string literal |
| 4:09:28 | Syntax errors must be fixed; they cannot be caught |
| 4:10:36 | number.py: the hazard in int(input(...)) |
| 4:13:27 | Edge-case testing: 0, negatives, and cat |
| 4:14:09 | ValueError: invalid literal for int() |
| 4:16:05 | try / except ValueError |
| 4:19:00 | Can you catch everything with a bare except? (Not advised.) |
| 4:20:29 | Keep the try block around the line that can fail |
| 4:21:38 | The NameError that appears after moving the print |
| 4:25:52 | try / except / else: continue only on success |
| 4:29:41 | Use while True to ask again rather than quit |
| 4:33:40 | Where to put break, and its relation to return |
| 4:36:44 | Both designs are valid: readability vs a minimal try block |
| 4:37:07 | Packaging get_int() |
| 4:40:39 | Replacing break plus return with a single return |
| 4:42:48 | pass: catch it and stay quiet |
| 4:48:47 | Adding a prompt parameter to get_int |
| 4:51:08 | A preview of raise |
Two kinds of error
Syntax errors
print("hello, world)
SyntaxError: unterminated string literal
"Unterminated" means you started something and never stopped it; a "literal" is something you typed directly rather than a variable. In plain terms: you opened a string and never closed it.
The defining feature of a syntax error is that the program never runs. It is not a failure during execution; the code itself is invalid. That is why try / except cannot catch it — the interpreter rejects the file while parsing it.
Runtime errors
x = int(input("What's x? "))
print(f"x is {x}")
Typing 50, 0 or -1 is fine. Typing cat gives:
ValueError: invalid literal for int() with base 10: 'cat'
ValueError means the value handed to int was not valid. Your code is correct; the problem is the user — and the user's behaviour is something you cannot know in advance.
Runtime errors happen during execution, so they can be anticipated and handled in code.
Takeaway: a syntax error is "you wrote it wrong"; a runtime error is "the world did not behave as you expected".
try / except
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
The order of execution:
- attempt the
tryblock; - if no exception is raised, skip
exceptand runelse; - if a
ValueErroris raised, abandon the rest oftry, runexcept, and skipelse.
The type in except ValueError must be spelled correctly and is case-sensitive.
Why not a bare except
Python allows except: with no type, catching everything. The lecture argues against it:
- if you do not know what went wrong, you cannot handle it correctly;
- it swallows real bugs, so the program keeps running while quietly broken.
The better habit is to name the exception types you expect.
Keep the try block small
try:
x = int(input("What's x? "))
print(f"x is {x}")
except ValueError:
print("x is not an integer")
This runs, but print is inside try too. A print will essentially never raise ValueError, and including it blurs which line is actually at risk. Prefer:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
A subtle trap: NameError
Move the print out of try but leave it unconditional:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
print(f"x is {x}")
Typing cat now gives:
NameError: name 'x' is not defined
The reason is assignment happens last. In x = int(input(...)), Python evaluates the right side first: input returns "cat", int("cat") raises ValueError. The exception aborts the line before the right-hand value exists, so the assignment never runs and x is never created.
except only catches the error; it does not give x a value. The later print then refers to a name that does not exist and raises a second error.
else exists precisely for this: run only when try succeeded.
Asking again in a loop
The program above gives up after one bad input, which is unfriendly. Keep asking:
while True:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
break
print(f"x is {x}")
Now cat produces a message and another prompt, until a valid integer arrives.
Note that break sits in else: else is attached to try, not to except, so it runs only when no exception occurred.
You can also drop else and put break at the end of try:
while True:
try:
x = int(input("What's x? "))
break
except ValueError:
print("x is not an integer")
Both are correct. The lecture's position is that either is good design as long as you can explain the choice — the trade-off is between a minimal try block and one less layer of nesting.
Packaging get_int
Once the same validation is written twice, extract it:
def main():
x = get_int("What's x? ")
print(f"x is {x}")
def get_int(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
pass
main()
The evolution is worth following:
- Write it plainly: convert,
breakon success,return xafter the loop. - Merge: since
return ximmediately followsbreak, usereturn xdirectly —returnboth exits the loop and hands back the value. - Compress:
xis used once, so drop it andreturn int(input(prompt)). - Parameterise: let the caller supply the prompt, so
get_intis not hard-wired to ask forx. It can now ask forx,yorage.
main is then two lines: get a number, print it. The complexity sits inside the function.
pass: catch and say nothing
pass in an except block means "I caught it and will do nothing further". The program does not crash, prints no error, and simply loops round to prompt again.
while True:
try:
return int(input(prompt))
except ValueError:
pass
The user sees only the prompt repeating. Whether that is better depends on the situation: quiet may feel friendlier, or it may leave the user puzzled about what went wrong.
Two styles: EAFP and LBYL
The Python community leans toward EAFP — Easier to Ask Forgiveness than Permission: do it, then handle failure.
try:
x = int(input("What's x? "))
except ValueError:
...
Other languages favour LBYL — Look Before You Leap: check first, then act.
s = input("What's x? ")
if s.isnumeric():
x = int(s)
else:
...
Both are reasonable. Python's inertia is toward EAFP, but checking first works perfectly well; the important part is knowing which style you chose.
Chapter summary
- Syntax errors cannot be caught and must be fixed; runtime errors can be handled in code.
ValueErrormeans "invalid value";int("cat")raises it.try/exceptseparates risky code from the response to failure.- Do not write a bare
except:; name the exception types. - Keep
tryaround only the line that can actually fail. - Assignment happens last: if the right side fails, the left side never gets a value, and later access raises
NameError. elseruns only whentrysucceeded; it belongs totry.while Truewithtry/except/breakis the standard validation structure.returnexits the loop and returns a value, which is tidier thanbreakfollowed byreturn.passcatches and silently ignores.- Wrapping "get a valid integer" in
get_int(prompt)leaves callers to worry only about the result.
Comprehension check
1. What happens when the following receives cat, and why? How should it be fixed?
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
print(f"x is {x}")
Reference answer: it raises NameError: name 'x' is not defined. In x = int(input(...)) the right side is evaluated first: int("cat") raises ValueError, which aborts the line. The right-hand value never exists, so the assignment never happens and x is never created. except catches the error but does not assign anything.
Fix it with else, so the print runs only on success:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}")
2. Is else attached to try or to except? When does it run?
Reference answer: attached to try. It runs only when the try block raises no exception at all. If try raises a caught exception, the except block runs and else is skipped; if it raises an uncaught exception, the error propagates and else does not run either.
3. Why is a bare except: discouraged?
Reference answer: a bare except: catches every exception, including types you never anticipated. That causes two problems: you do not know what actually happened, so you cannot respond correctly; and it swallows genuine bugs, so the program keeps running while broken. The correct practice is to name the expected exception types, such as except ValueError.
4. What is wrong with putting print(f"x is {x}") inside the try block?
Reference answer: print will essentially never raise ValueError, so including it widens the "might fail" region and makes it unclear which line is actually being protected. The try should cover the smallest region that can raise — normally just int(input(...)). That keeps the boundary of error handling explicit and avoids catching the same exception type from unrelated code.
5. After converting number.py to the while True version, why does entering cat not terminate the program?
Reference answer: because except ValueError catches the exception. After try fails, execution continues to the end of the loop body, the while True condition is still true, and the loop prompts again. The loop ends only when try succeeds and else runs break. Since the exception is caught, it no longer propagates out of the program.
6. How could the following get_int be improved?
def get_int():
while True:
try:
x = int(input("What's x? "))
except ValueError:
print("x is not an integer")
else:
break
return x
Reference answer: merge break and return — put return x in else and the post-loop return disappears:
def get_int(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("x is not an integer")
return both ends the loop and hands back the value. Also, the prompt is hard-coded, so the function can only ask for x; adding a prompt parameter makes it reusable for any variable. If the repeated message is unwanted, the except block can become pass.
Relation to neighbouring chapters
Exception handling turns the previous chapter's input validation from "assuming the user cooperates" into "continuing even when they do not", which is the foundation of robust programs. The next chapter on libraries introduces many external functions that can raise, and try / except is the tool for them.
The get_int(prompt) pattern — wrap a fallible operation plus a retry, expose only the result — recurs in file I/O and unit testing.