0%

异常处理

Python 异常处理:别让程序因为一个小错误就崩了

写程序最怕什么?不是 bug 修不完,而是程序跑着跑着突然崩了,日志里一堆红字,用户看到的是”服务器错误”。

异常处理就是干这个的——预料到可能会出错的地方,提前想好怎么兜底。Python 的 try...except 就是给代码加一道防护网,出错了不至于整个程序炸掉。

基础结构:try...except

1
2
3
4
5
6
try:
# 可能发生异常的代码块
result = 1 / 0 # 除数为 0,会触发 ZeroDivisionError
except ZeroDivisionError as e:
# 捕获特定异常并处理
print(f"发生错误:{e}") # 输出:发生错误:division by zero

try 里面放”可能出错的代码”,except 里面放”出错之后怎么办”。

为什么要有这东西? 如果没有 try...except,程序直接崩溃退出,用户一脸懵。有了它,程序可以继续跑,顶多那条数据处理失败了。

捕获多种异常

可使用多个 except 块处理不同类型的异常,或在一个 except 中捕获多个异常:

1
2
3
4
5
6
7
8
9
10
try:
num = int(input("请输入一个整数:"))
result = 10 / num
except ValueError:
print("输入错误:请输入有效的整数")
except ZeroDivisionError:
print("计算错误:除数不能为 0")
except (TypeError, OverflowError):
# 同时捕获多种异常(用元组包裹)
print("其他类型错误")

也可以这样写,一个except捕获多个异常

1
2
3
4
5
try:
num = int(input("请输入数字:"))
result = 10 / num
except (ValueError, ZeroDivisionError) as e:
print(f"出错了:{e}")

捕获所有异常(不推荐)

使用 except Exception 可捕获所有非系统退出的异常(不包括 KeyboardInterrupt 等):

1
2
3
4
5
6
try:
# 任意可能出错的代码
lst = [1, 2, 3]
print(lst[10]) # 索引越界,触发 IndexError
except Exception as e:
print(f"发生未知错误:{e}") # 输出:发生未知错误:list index out of range

except Exception 能捕获几乎所有常见的异常,看起来省事,但有隐患

为什么不推荐一味地捕获所有异常?

  • 把不该忽略的异常也吞掉了
  • 隐藏了代码里的 bug,本来应该修的问题变成了”程序还在跑但逻辑不对”

什么时候可以捕获所有异常? 只在程序的最顶层(比如 main 函数)做统一兜底,记录日志后优雅退出:

1
2
3
4
5
6
def main():
try:
run_app()
except Exception as e:
logger.error(f"程序崩溃:{e}")
sys.exit(1)

其他地方尽量捕获具体的异常类型。

完整语法:try-except-else-finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try:
# 可能抛出异常的代码
pass
except SomeError as e:
# 捕获特定异常时执行
pass
except AnotherError as e:
# 可以多个 except 块
pass
except Exception as e:
# 捕获所有其他异常(兜底)
pass
else:
# 仅在 try 块无异常时执行
pass
finally:
# 无论是否发生异常,始终执行(用于清理资源)
pass

else:没事的时候跑一下

else 块在 try 没有抛出任何异常的时候执行。

1
2
3
4
5
6
try:
result = 10 / 2
except ZeroDivisionError:
print("除数不能为0")
else:
print(f"计算结果:{result}") # 只有除法成功才执行

为什么不用 else,直接把代码放 try 里? 因为放在 try 里的所有代码都在同一个”防护罩”里,如果把后续处理也放进去,后续代码出错了会被误解为除法的错误。用 else 能把这层区分清楚。

finally:无论如何都要做的事

finally 不管发没发生异常都会执行,通常用来释放资源——关闭文件、关闭数据库连接、释放锁。

1
2
3
4
5
6
7
8
9
file = None
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
if file:
file.close() # 不管成不成功,文件都要关

一个需要注意的地方: finally 里不要写 return,它会覆盖 try 里的返回值。

1
2
3
4
5
6
7
def test():
try:
return 1
finally:
return 2 # 这个 return 会覆盖上面的

test() # 返回 2,不是 1

手动抛出异常(raise

有时候你觉得某个条件不满足,想主动让程序报错,用 raise

1
2
3
4
5
6
7
8
9
10
def check_age(age):
if age < 0:
# 手动抛出 ValueError 异常
raise ValueError(f"年龄不能为负数:{age}")
print(f"年龄是:{age}")

try:
check_age(-5)
except ValueError as e:
print(f"捕获到错误:{e}") # 输出:捕获到错误:年龄不能为负数:-5

raise…from:异常链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DataLoadError(Exception):
pass

def load_config():
try:
with open("config.json") as f:
return f.read()
except FileNotFoundError as e:
# 抛出自定义异常,并保留原始异常作为原因
raise DataLoadError("配置文件加载失败") from e

try:
config = load_config()
except DataLoadError as e:
print(f"错误:{e}")
print(f"原始原因:{e.__cause__}") # FileNotFoundError

raise ... from e 能把原始异常带过去,排查问题的时候知道根因是什么。

抛出自定义异常

除了 Python 内置异常(如 ValueErrorTypeError),还可定义自定义异常类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义自定义异常(继承自 Exception)
class InvalidScoreError(Exception):
"""分数无效异常(分数必须在 0-100 之间)"""
pass

def check_score(score):
if not (0 <= score <= 100):
# 抛出自定义异常
raise InvalidScoreError(f"无效分数:{score}(必须在 0-100 之间)")

try:
check_score(150)
except InvalidScoreError as e:
print(f"错误:{e}") # 输出:错误:无效分数:150(必须在 0-100 之间)

自定义异常的好处是:调用你代码的人能精准地捕获和处理业务层面的错误,而不是去判断一堆 ValueError

内置异常体系

异常层次结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── TypeError
├── ValueError
├── AttributeError
├── IOError / OSError
│ └── FileNotFoundError
└── ...

常用内置异常速查表

异常类型 触发场景 示例
ValueError 值类型正确但值无效 int("abc")
TypeError 操作类型不匹配 "2" + 3
IndexError 序列索引超出范围 [1,2,3][5]
KeyError 字典键不存在 {"a":1}["b"]
AttributeError 对象没有该属性 "str".unknown()
ZeroDivisionError 除数为0 1 / 0
FileNotFoundError 文件不存在 open("no.txt")
ImportError 导入模块失败 import nonexist
StopIteration 迭代器没有下一个元素 next(iter([]))
KeyboardInterrupt 用户按 Ctrl+C -

捕获 KeyboardInterrupt(优雅退出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time

def main():
try:
while True:
print("运行中...")
time.sleep(1)
except KeyboardInterrupt:
print("\n收到退出信号,正在清理资源...")
# 执行清理操作
print("程序已安全退出")

if __name__ == "__main__":
main()

异常与上下文管理器

with 语句自动管理资源

Python 的 with 语句通过上下文管理器自动处理资源的获取和释放,即使发生异常也会自动清理。

1
2
3
4
5
6
7
8
9
10
#  使用 with 管理文件(无需手动 finally)
try:
with open("data.txt", "r") as file:
content = file.read()
number = int(content)
except FileNotFoundError:
print("文件不存在")
except ValueError:
print("文件内容不是数字")
# 文件会自动关闭,无论是否发生异常

自定义上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name

def __enter__(self):
print(f"连接到数据库 {self.db_name}")
self.conn = f"connection_{self.db_name}"
return self.conn

def __exit__(self, exc_type, exc_val, exc_tb):
print(f"关闭数据库连接 {self.conn}")
if exc_type:
print(f"发生异常:{exc_val}")
return False # 返回 True 表示抑制异常

# 使用
try:
with DatabaseConnection("test") as conn:
print(f"使用 {conn} 执行查询")
raise ValueError("查询错误")
except ValueError:
print("外层捕获异常")
# 输出:
# 连接到数据库 test
# 使用 connection_test 执行查询
# 关闭数据库连接 connection_test
# 发生异常:查询错误
# 外层捕获异常

欢迎关注我的其它发布渠道