程序的稳健之道:面对错误
在编程的世界里,无论你多么细心,程序总会遇到各种意外情况:
- 用户在要求输入年龄的地方输入了英文字母;
- 程序试图打开一个被用户删除或改名的文件;
- 试图进行数学上的除以零;
- 访问了一个不存在的字典键。
如果程序一旦遇到错误就直接崩溃退出,这在生产环境中是不可接受的。Python 官方教程第 8 章为我们系统讲解了**错误和异常(Errors and Exceptions)**的应对机制。
1. 句法错误 vs 运行时异常
Python 中至少有两种截然不同的错误:
句法错误(Syntax Errors)
也被称为解析错误(parsing error)。它是指代码在语法结构上违反了 Python 的规定(比如漏写了冒号 :,括号没有闭合,或者多敲了非法符号):
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
解释器在运行任何一行代码之前,就会对语法进行扫描解析。一旦发现句法错误,程序甚至连启动都无法启动。
运行时异常(Exceptions)
即使语句在语法上完全合规,但在实际运行求值时,依然可能由于环境或数据原因引发错误。这种在运行期间被检测到的错误被称为异常(Exception):
ZeroDivisionError: division by zero(除以零);NameError: name 'spam' is not defined(访问未定义的变量);TypeError: can only concatenate str (not "int") to str(类型不匹配);FileNotFoundError: [Errno 2] No such file or directory(文件不存在);IndexError: list index out of range(下标越界);KeyError: 'foo'(字典中不存在该键)。
2. 捕获与处理异常:try…except 语句
为了防止异常导致整个程序非正常崩溃中断,我们使用 try...except 语句来主动捕获并妥善处理它们:
while True:
try:
x = int(input("Please enter a number: "))
break
except ValueError:
print("Oops! That was no valid number. Try again...")
执行流程拆解:
- 首先执行
try缩进块中的语句; - 如果
try块内没有发生任何异常,except分支就会被完全忽略,程序顺利往下走; - 如果
try块执行到某一行发生异常(例如输入了字母导致int()抛出ValueError),try块中剩余的代码会被立即跳过,Python 会去匹配相应的except分支; - 如果发生的异常类型与
except后面声明的异常名字匹配,就执行该except块中的补救代码; - 如果发生的异常在所有
except中都没有被捕获,这个异常就会继续向外层抛出,最终导致程序报错终止并打印堆栈轨迹。
捕获多个异常与捕获错误对象
一个 try 语句可以有多个 except 子句,分别应对不同类型的异常;也可以把多个异常类型写成一个**元组(tuple)**合并处理:
try:
with open('data.txt', 'r', encoding='utf-8') as f:
content = f.read()
value = int(content)
except (FileNotFoundError, PermissionError):
print("文件不存在或没有权限读取!")
except ValueError:
print("文件内容无法转换为有效整数!")
except OSError as err:
# 使用 as 关键字将异常对象绑定到变量,打印官方底层错误信息
print(f"OS error: {err}")
注意:不要写光秃秃的
except:如果写没有任何类型的裸except:,它会连用户按下Ctrl+C想要中断程序的键盘信号都一起强行吞掉,这会严重掩盖真实的程序 Bug。务必明确指定要捕获的异常类型!
3. else 与 finally 子句
try...except 还可以附带另外两个强大的子句:
1. else 子句(可选)
else 必须跟在所有 except 子句之后。它只在 try 块没有发生任何异常时才会执行:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r', encoding='utf-8')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
将仅需在正常情况下执行的代码放在 else 块中,可以避免在 try 块中放过多语句而意外捕获到非预期的异常。
2. finally 清理子句(可选)
finally 放在整个异常处理结构的最末尾。无论前面是否发生异常、是否捕获成功,甚至即使前面执行了 return、break 语句,finally 块中的代码都必定会被百分之百执行:
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("division by zero!")
else:
print("result is", result)
finally:
print("executing finally clause")
divide(2, 1)
# 输出:
# result is 2.0
# executing finally clause
divide(2, 0)
# 输出:
# division by zero!
# executing finally clause
finally 常用于必须关闭的网络连接、释放锁或临时文件清理等兜底收尾工作。
4. 主动抛出异常:raise 语句
不仅解释器会自动抛出异常,你在编写自己的逻辑时,一旦发现传入的数据不符合业务规则,也可以使用 raise 语句主动抛出一个异常:
def validate_age(age):
if age < 0:
raise ValueError("年龄不能为负数!")
return age
还可以定义自己的专用异常类,只需让它继承自标准库的 Exception 类即可。
学会了错误与异常的捕获和防御,我们的代码就具备了抵御意外风浪的坚固护甲。在下一关中,我们将探索现代编程中最为重要的范式之一——类与面向对象编程!