异常处理概述

异常处理是指程序在运行过程中遇到错误时,通过特定的语法结构来捕获、处理错误,防止程序意外崩溃

在编写程序时,我们无法预测所有可能发生的错误:文件可能不存在、网络可能断开、用户可能输入非法数据。如果不对这些情况进行处理,程序就会直接崩溃退出。

来看一个没有异常处理的例子:

1
2
3
4
5
6
7
# 用户输入一个数字,程序计算 100 除以这个数字
num = int(input("请输入一个数字:"))
result = 100 / num
print(f"100 / {num} = {result}")

# 如果用户输入 0,程序崩溃:ZeroDivisionError: division by zero
# 如果用户输入 abc,程序崩溃:ValueError: invalid literal for int()

通过异常处理,我们可以让程序"优雅地"应对这些意外情况:

1
2
3
4
5
6
7
"""
异常处理的核心思路:
1. 尝试执行可能出现问题的代码
2. 如果出现错误,捕获它
3. 根据错误类型做出相应的处理
4. 程序继续运行,不会崩溃
"""

常见异常类型

Python 内置了丰富的异常类型,不同错误对应不同的异常类,了解它们有助于精确地处理各种错误

Python 中常见的异常类型:

异常类 触发条件 示例
NameError 使用了未定义的变量 print(unknown_var)
TypeError 操作类型不匹配 "hello" + 5
ValueError 值不合法 int("abc")
ZeroDivisionError 除数为零 100 / 0
IndexError 列表索引越界 [1,2,3][10]
KeyError 字典键不存在 {"a":1}["b"]
FileNotFoundError 文件不存在 open("no.txt")
AttributeError 对象没有该属性 "str".append()
ImportError 导入模块失败 import not_exist
SyntaxError 语法错误 if True print()
IndentationError 缩进错误 混用空格和Tab
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
# 直观感受各种异常
# 取消注释以下代码逐条测试

# NameError — 变量未定义
# print(x)

# TypeError — 类型不匹配
# result = "年龄:" + 18

# ValueError — 值不合法
# num = int("一百二十三")

# ZeroDivisionError — 除零错误
# print(10 / 0)

# IndexError — 索引越界
# nums = [1, 2, 3]
# print(nums[100])

# KeyError — 键不存在
# student = {"name": "小明", "age": 18}
# print(student["score"])

# AttributeError — 属性不存在
# text = "hello"
# text.append("world")
1
2
3
4
5
6
7
8
9
10
11
12
# 查看异常的继承关系
"""
所有内置异常都继承自 BaseException
程序逻辑相关的异常继承自 Exception
了解继承关系有助于理解异常捕获的匹配规则
"""

# 查看异常的类型
print(ZeroDivisionError.__bases__) # (<class 'ArithmeticError'>,)
print(ArithmeticError.__bases__) # (<class 'Exception'>,)
print(FileNotFoundError.__bases__) # (<class 'OSError'>,)
print(OSError.__bases__) # (<class 'Exception'>,)

try-except 基础用法

使用 try-except 结构捕获异常:把可能出错的代码放入 try 块,在 except 块中编写错误处理逻辑

  1. 基本语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"""
try-except 基本结构:

try:
# 可能发生异常的代码
risky_operation()
except 异常类型:
# 发生异常时的处理代码
handle_error()
"""

# 示例:安全地执行除法
try:
a = int(input("被除数:"))
b = int(input("除数:"))
result = a / b
print(f"结果:{result}")
except ZeroDivisionError:
print("错误:除数不能为 0")
except ValueError:
print("错误:请输入有效的整数")

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
# 对比:没有异常处理 vs 有异常处理

# 场景一:没有异常处理
def unsafe_division():
a = int(input("被除数:"))
b = int(input("除数:"))
return a / b

# result = unsafe_division() # 输入 0 就崩溃了
# print("这行可能永远不会执行")

# 场景二:有异常处理
def safe_division():
try:
a = int(input("被除数:"))
b = int(input("除数:"))
return a / b
except ZeroDivisionError:
print("除数不能为 0")
except ValueError:
print("请输入有效整数")
return None # 出错时返回 None

result = safe_division()
print(f"安全除法的结果:{result}")
  1. 捕获异常并获取错误信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""
使用 as 关键字将异常对象绑定到一个变量
通过该变量可以获取具体的错误信息
"""
try:
num = int("abc")
except ValueError as e:
print(f"捕获到 ValueError")
print(f"错误信息:{e}")
print(f"异常类型:{type(e).__name__}")

# 输出:
# 捕获到 ValueError
# 错误信息:invalid literal for int() with base 10: 'abc'
# 异常类型:ValueError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 实际应用:向用户展示友好的错误提示
def read_file_safely(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError as e:
print(f"❌ 文件不存在:{filename}")
print(f" 详细信息:{e}")
except PermissionError as e:
print(f"❌ 没有权限读取:{filename}")
print(f" 详细信息:{e}")
except Exception as e:
print(f"❌ 发生了未知错误:{e}")
return None

# 测试
content = read_file_safely("不存在的文件.txt")
print(f"读取结果:{content}")
  1. 捕获所有异常(慎用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"""
except 后面不指定异常类型,会捕获所有异常
但这种方式不推荐:
- 它会捕获 SyntaxError、KeyboardInterrupt 等不该捕获的异常
- 会掩盖程序中真正的逻辑错误
- 调试时会很难定位问题

更好的做法:至少指定 except Exception,这排除了系统级异常
"""

# 不推荐:裸 except
try:
risky_code()
except: # 捕获所有异常,包括 Ctrl+C 中断
print("出错了")

# 推荐:至少指定 Exception
try:
risky_code()
except Exception as e: # 只捕获程序逻辑异常
print(f"出错了:{e}")

完整的异常处理结构

try-except-else-finally 构成了完整的异常处理流程,每个子句各司其职

  1. 四种子句的角色
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
"""
完整的异常处理结构:

try:
# 尝试执行的代码(监控区)
pass
except 异常类型:
# 发生异常时执行(处理区)
pass
else:
# 没有发生异常时执行(成功区)
pass
finally:
# 无论是否发生异常都执行(清理区)
pass
"""

# 示例:读取文件并解析数据
def load_config(filename):
config = {}
f = None
try:
f = open(filename, 'r', encoding='utf-8')
content = f.read()
# 解析配置:每行 key=value
for line in content.strip().split('\n'):
key, value = line.split('=')
config[key.strip()] = value.strip()
except FileNotFoundError:
print(f"配置文件 {filename} 不存在,使用默认配置")
except ValueError as e:
print(f"配置格式错误:{e}")
else:
print(f"配置文件 {filename} 加载成功,共 {len(config)} 项")
finally:
# 无论如何都要关闭文件
if f:
f.close()
print("文件已关闭")
return config

# 假设 config.ini 不存在
settings = load_config("config.ini")
  1. else 子句的使用场景
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
29
30
"""
else 子句只在 try 块没有发生异常时执行
使用 else 而非把代码直接放在 try 块中,有几个好处:
1. 语义更清晰:这些代码只在"一切正常"时运行
2. 避免意外捕获 else 代码中抛出的异常
3. 减小 try 块的代码范围,明确哪些代码需要被监控
"""

# 不推荐:把正常逻辑也放进 try
def process_data_bad(raw_data):
try:
num = int(raw_data)
result = 100 / num
print(f"计算成功,结果:{result}")
log_result(result) # log_result 的异常也会被捕获,难以定位问题
except (ValueError, ZeroDivisionError) as e:
print(f"输入有误:{e}")

# 推荐:用 else 隔离"成功后的逻辑"
def process_data_good(raw_data):
try:
num = int(raw_data)
result = 100 / num
except ValueError:
print("请输入有效的数字")
except ZeroDivisionError:
print("除数不能为 0")
else:
print(f"计算成功,结果:{result}")
log_result(result) # 如果这里报错,不会被 except 捕获,便于调试
  1. finally 子句的使用场景
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
29
30
31
32
33
34
35
36
"""
finally 子句无论是否发生异常都会执行
典型用途:
- 关闭文件/数据库连接/网络连接
- 释放锁
- 恢复环境状态
"""

# 示例一:文件写入——确保数据落盘
def write_data(filename, data):
f = open(filename, 'w', encoding='utf-8')
try:
for item in data:
f.write(str(item) + '\n')
finally:
f.close() # 即使写入过程中出错,也要关闭文件
print(f"文件 {filename} 已关闭")

# 示例二:无论成功失败都给出反馈
def connect_server(address):
import time
print(f"正在连接 {address}...")
start = time.time()
try:
# 模拟连接过程
time.sleep(1)
# raise ConnectionError("连接超时")
print("连接成功")
except ConnectionError as e:
print(f"连接失败:{e}")
else:
print("正在进行数据传输...")
finally:
elapsed = time.time() - start
print(f"操作耗时:{elapsed:.1f} 秒")
print("连接已关闭(如有必要)")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# finally 的特殊之处:即使在 try/except 中 return 了,finally 仍会执行
def demo_finally():
try:
print("try 块")
return "try 的返回值"
except:
print("except 块")
finally:
print("finally 块 — 无论如何都会执行")
print("这行永远不会执行") # 不可达代码

result = demo_finally()
print(f"函数返回值:{result}")

# 输出:
# try 块
# finally 块 — 无论如何都会执行
# 函数返回值:try 的返回值

捕获多种异常

根据不同的异常类型分别处理,也可以用元组一次捕获多种异常,还可以通过异常继承关系实现层级捕获

  1. 多个 except 子句
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
"""
一个 try 可以对应多个 except 子句
按顺序匹配:从上到下,匹配到第一个符合的 except 后就停止
注意:子类异常要放在父类异常前面!
"""

def safe_operation(data):
try:
# 尝试各种可能出错的操作
value = int(data)
result = 100 / value
items = [10, 20, 30]
print(f"取出的元素:{items[value]}")
return result
except ValueError:
print("ValueError:数据无法转换为整数")
except ZeroDivisionError:
print("ZeroDivisionError:不能除以零")
except IndexError:
print("IndexError:索引超出了列表范围")
except Exception as e:
print(f"其他未预料的异常:{e}")

# 测试不同输入
safe_operation("abc") # ValueError
safe_operation("0") # ZeroDivisionError
safe_operation("5") # IndexError
safe_operation("1") # 正常
  1. 用元组一次捕获多种异常
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
"""
当多种异常的处理方式相同时,可以用元组将它们放在一个 except 中
"""

# 分别处理(代码重复)
def process_input_verbose(data):
try:
num = int(data)
return 100 / num
except ValueError:
print("输入不合法,请重新输入")
return None
except ZeroDivisionError:
print("输入不合法,请重新输入") # 重复代码!
return None

# 用元组合并处理(简洁)
def process_input_clean(data):
try:
num = int(data)
return 100 / num
except (ValueError, ZeroDivisionError) as e:
print(f"输入不合法({e}),请重新输入")
return None

# 测试
process_input_clean("abc")
process_input_clean("0")
  1. 异常继承与捕获顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
异常的捕获是"向上兼容"的——捕获父类异常会同时捕获其所有子类异常
因此,子类异常必须放在父类异常的前面
"""

# 错误示例:父类在前,子类永远匹配不到
def wrong_order():
try:
f = open("nofile.txt")
except Exception as e:
print(f"Exception 捕获了:{e}")
except FileNotFoundError as e: # 永远不会执行!
print(f"FileNotFoundError 捕获了:{e}")

# 正确示例:子类在前,父类在后(兜底)
def right_order():
try:
f = open("nofile.txt")
except FileNotFoundError as e:
print(f"文件没找到:{e}")
except OSError as e:
print(f"其他 I/O 错误:{e}")
except Exception as e:
print(f"其他未预料的错误:{e}")

异常的传播

当异常在一个函数内部没有被捕获时,它会向上传播到调用它的函数,直到被捕获或导致程序终止

  1. 异常传播机制
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
29
30
31
32
33
"""
异常传播(冒泡)规则:
1. 函数内部发生异常且未被捕获 → 传播到调用方
2. 调用方未捕获 → 继续向上传播
3. 一直传播到全局作用域仍未被捕获 → 程序终止并打印 Traceback
"""

def level3():
print("level3 开始")
result = 1 / 0 # 产生 ZeroDivisionError,但未捕获
print("level3 结束") # 不会执行

def level2():
print("level2 开始")
level3() # level3 的异常传播到这里
print("level2 结束") # 不会执行

def level1():
print("level1 开始")
try:
level2()
except ZeroDivisionError:
print("level1 捕获了 ZeroDivisionError!")
print("level1 结束")

level1()

# 输出:
# level1 开始
# level2 开始
# level3 开始
# level1 捕获了 ZeroDivisionError!
# level1 结束
  1. 在哪里捕获异常?
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""
原则:在"知道如何处理"的层级捕获异常

- 底层函数:让异常自然传播,不要随意吞掉
- 中间层:可以包装异常,添加上下文信息后重新抛出
- 顶层:统一捕获并处理(日志记录、用户提示等)
"""

# 底层:函数只关注自己的逻辑,不吞异常
def parse_line(line):
"""解析一行数据,格式:name:score"""
name, score_str = line.split(':')
return name, int(score_str)
# 如果格式不对,ValueError 自然向上传播

# 中间层:添加上下文信息后重新抛出
def load_scores(filename):
"""从文件加载所有成绩"""
scores = {}
try:
with open(filename, 'r', encoding='utf-8') as f:
for i, line in enumerate(f, 1):
try:
name, score = parse_line(line.strip())
scores[name] = score
except (ValueError, TypeError) as e:
# 包装异常,加上行号信息,便于定位问题
raise ValueError(f"第{i}行格式错误:'{line.strip()}'") from e
except FileNotFoundError:
raise FileNotFoundError(f"成绩文件 '{filename}' 不存在")
return scores

# 顶层:统一处理,给用户友好的提示
def main():
try:
scores = load_scores("scores.txt")
print(f"加载了 {len(scores)} 条成绩")
except (FileNotFoundError, ValueError) as e:
print(f"程序出错:{e}")
print("请检查文件是否存在且格式正确")
except Exception as e:
print(f"未知错误:{e}")

# main()

抛出异常

使用 raise 关键字手动抛出异常,可以在特定条件下主动触发错误处理流程

  1. raise 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""
raise 异常类(错误信息):主动抛出一个异常

使用场景:
- 输入参数不合法
- 业务规则被违反
- 状态不一致
"""

def set_age(age):
"""设置年龄,必须为 0-150 之间的整数"""
if not isinstance(age, int):
raise TypeError(f"年龄必须是整数,而输入了 {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"年龄超出合理范围:{age}(应为 0-150)")
print(f"年龄已设置为:{age}")
return age

# 测试
# set_age(200) # ValueError: 年龄超出合理范围:200(应为 0-150)
# set_age("18") # TypeError: 年龄必须是整数,而输入了 str
set_age(25) # 正常
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
# 在实际项目中的应用
def transfer_money(sender, receiver, amount):
"""转账操作"""
if amount <= 0:
raise ValueError(f"转账金额必须大于 0,当前:{amount}")
if sender == receiver:
raise ValueError("不能给自己转账")
if amount > sender["balance"]:
raise ValueError(
f"余额不足:需要 {amount} 元,当前余额 {sender['balance']} 元"
)

# 执行转账
sender["balance"] -= amount
receiver["balance"] += amount
print(f"转账成功:{sender['name']}{receiver['name']},金额 {amount} 元")

# 使用
alice = {"name": "Alice", "balance": 1000}
bob = {"name": "Bob", "balance": 500}

try:
transfer_money(alice, bob, 1200) # 余额不足
except ValueError as e:
print(f"转账失败:{e}")

transfer_money(alice, bob, 200) # 正常
  1. 重新抛出异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""
在 except 块中可以用 raise(不带参数)重新抛出当前异常
这在"记录日志但不吞异常"的场景中很有用
"""

def process_with_logging(data):
try:
result = risky_operation(data)
except Exception as e:
# 记录日志
print(f"[ERROR] 处理数据 '{data}' 时发生异常:{e}")
# 重新抛出,让上层也能感知到这个异常
raise
return result

# 也可以使用 raise ... from ... 建立异常链
def load_user_config(user_id):
try:
user_data = database.query(user_id)
except DatabaseError as e:
# 包装为更具体的业务异常
raise ConfigLoadError(f"加载用户 {user_id} 配置失败") from e

自定义异常

当内置异常无法准确表达业务错误时,可以创建自定义异常类,继承自 Exception

  1. 创建简单的自定义异常
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
29
30
31
"""
自定义异常类只需要继承 Exception(或其子类)
通常只需定义一个类体,可以添加自定义属性
"""

# 定义自定义异常
class InsufficientBalanceError(Exception):
"""余额不足异常"""
pass

class InvalidPasswordError(Exception):
"""密码格式错误异常"""
pass

class AccountLockedError(Exception):
"""账户被锁定异常"""
pass

# 使用自定义异常
def withdraw(account, amount):
"""取款操作"""
if account["locked"]:
raise AccountLockedError(f"账户 {account['name']} 已被锁定")
if amount <= 0:
raise ValueError("取款金额必须大于 0")
if amount > account["balance"]:
raise InsufficientBalanceError(
f"余额不足:需要 {amount} 元,可用 {account['balance']} 元"
)
account["balance"] -= amount
print(f"取款成功:{amount} 元,剩余 {account['balance']} 元")
  1. 带属性的自定义异常
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
29
30
31
32
33
34
35
36
37
38
39
"""
自定义异常可以添加额外的属性来携带更多错误信息
这有助于调用方更精确地处理异常
"""

class ValidationError(Exception):
"""数据校验异常,携带校验失败的字段和原因"""
def __init__(self, field, value, reason):
self.field = field # 哪个字段校验失败
self.value = value # 不合法的值
self.reason = reason # 失败原因
message = f"字段 '{field}' 的值 '{value}' 不合法:{reason}"
super().__init__(message)

# 使用带属性的自定义异常
def validate_user(data):
"""校验用户输入"""
if not data.get("username"):
raise ValidationError("username", data.get("username"), "用户名不能为空")

password = data.get("password", "")
if len(password) < 6:
raise ValidationError("password", "***", "密码长度不能少于 6 位")

age = data.get("age")
if age is not None and (age < 0 or age > 150):
raise ValidationError("age", age, "年龄应在 0-150 之间")

print("数据校验通过")

# 捕获时可以利用自定义属性
test_data = {"username": "admin", "password": "123", "age": 200}
try:
validate_user(test_data)
except ValidationError as e:
print(f"校验失败:")
print(f" 字段:{e.field}")
print(f" 值:{e.value}")
print(f" 原因:{e.reason}")
  1. 自定义异常的层级结构
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"""
对于较大的项目,建立异常层级结构有助于分类处理

例如一个简单的电商系统:
"""

# 异常层级
class ECommerceError(Exception):
"""电商系统基础异常"""
pass

class PaymentError(ECommerceError):
"""支付相关异常"""
pass

class CreditCardDeclinedError(PaymentError):
"""信用卡被拒"""
pass

class InsufficientFundsError(PaymentError):
"""余额不足"""
pass

class InventoryError(ECommerceError):
"""库存相关异常"""
pass

class OutOfStockError(InventoryError):
"""库存不足"""
pass

# 使用异常层级:上层可以按粒度捕获
def checkout(cart, payment_method):
try:
# 复杂的结账流程...
validate_stock(cart)
process_payment(payment_method, cart["total"])
except OutOfStockError as e:
print(f"库存不足:{e},建议移除缺货商品")
except PaymentError as e:
# 捕获所有支付相关异常
print(f"支付失败:{e},请更换支付方式")
except ECommerceError as e:
# 捕获所有业务异常
print(f"结账失败:{e}")

断言

assert 语句用于在开发调试阶段快速检查"不应该发生"的情况,生产环境中可以全局关闭

  1. assert 基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
"""
assert 条件, 错误信息:
- 条件为 True:什么都不发生,程序继续
- 条件为 False:抛出 AssertionError 并显示错误信息
"""

def calculate_average(scores):
"""计算平均分,scores 不能为空列表"""
assert len(scores) > 0, "成绩列表不能为空"
return sum(scores) / len(scores)

print(calculate_average([85, 90, 78])) # 正常:84.333...
# print(calculate_average([])) # AssertionError: 成绩列表不能为空
1
2
3
4
5
6
7
8
9
10
11
12
13
# assert 的使用场景:检查"前置条件"和"后置条件"
def divide_safely(a, b):
"""除法运算,确保除数和结果都合法"""
# 前置条件:除数不能为 0
assert b != 0, "除数不能为 0"

result = a / b

# 后置条件:结果应该是一个有限数
import math
assert math.isfinite(result), f"计算结果不合法:{result}"

return result
  1. assert 与 raise 的区别
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
"""
assert vs raise:
┌──────────┬─────────────────┬──────────────────────┐
│ 方式 │ 典型用途 │ 可被关闭? │
├──────────┼─────────────────┼──────────────────────┤
│ assert │ 调试时检查逻辑 │ 是(python -O 关闭) │
│ raise │ 任何时候检查输入 │ 否 │
└──────────┴─────────────────┴──────────────────────┘

原则:
- 检查用户输入、外部数据 → 用 raise(永远需要校验)
- 检查程序内部逻辑错误("不可能发生"的事) → 用 assert(只用于调试)
"""

# 正确:用户输入用 raise 校验
def set_volume(level):
if not 0 <= level <= 100:
raise ValueError(f"音量应在 0-100,输入了 {level}")
print(f"音量:{level}")

# 正确:内部不变量用 assert 检查
def _sort_helper(items):
"""内部排序辅助函数,调用方已保证 items 是列表"""
assert isinstance(items, list), "内部错误:_sort_helper 需要列表参数"
items.sort()

综合练习

将所学知识融会贯通,通过实际案例巩固异常处理的使用

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
# 安全的用户输入验证器
"""
实现一个健壮的用户输入验证系统,能够处理各种输入异常
"""
def get_user_age():
"""反复提示用户输入年龄,直到得到合法值"""
while True:
try:
age_str = input("请输入您的年龄(0-150):")
age = int(age_str)
if age < 0 or age > 150:
raise ValueError(f"年龄 {age} 不在 0-150 范围内")
except ValueError as e:
print(f"输入无效:{e},请重新输入")
except KeyboardInterrupt:
print("\n用户取消了输入")
return None
except EOFError:
print("\n输入流已关闭")
return None
else:
print(f"您的年龄是:{age}")
return age

# get_user_age()
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 简单的文件配置管理器(完整的异常处理)
"""
从 JSON 配置文件读取配置,如果文件不存在或格式错误则使用默认配置
并具备安全写入的能力
"""
import json

class ConfigManager:
"""配置文件管理器,带有完整的异常处理"""

DEFAULT_CONFIG = {
"host": "localhost",
"port": 8080,
"debug": False,
"max_connections": 10
}

def __init__(self, config_file):
self.config_file = config_file
self.config = {}

def load(self):
"""加载配置,失败时使用默认值"""
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
loaded = json.load(f)
except FileNotFoundError:
print(f"[提示] 配置文件 {self.config_file} 不存在,使用默认配置")
self.config = self.DEFAULT_CONFIG.copy()
return self.config
except json.JSONDecodeError as e:
print(f"[错误] 配置文件格式错误:{e}")
print(f"[提示] 将使用默认配置")
self.config = self.DEFAULT_CONFIG.copy()
return self.config
except Exception as e:
print(f"[错误] 加载配置时发生意外错误:{e}")
self.config = self.DEFAULT_CONFIG.copy()
return self.config
else:
# 合并默认值和加载的配置(加载值优先)
self.config = {**self.DEFAULT_CONFIG, **loaded}
print(f"[成功] 配置文件 {self.config_file} 加载完成")
return self.config

def save(self):
"""保存配置到文件"""
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
except PermissionError:
print(f"[错误] 没有权限写入 {self.config_file}")
return False
except Exception as e:
print(f"[错误] 保存配置失败:{e}")
return False
else:
print(f"[成功] 配置已保存到 {self.config_file}")
return True

def get(self, key):
"""安全地获取配置项"""
try:
return self.config[key]
except KeyError:
print(f"[警告] 配置项 '{key}' 不存在")
return None

def set(self, key, value):
"""设置配置项"""
if key not in self.DEFAULT_CONFIG:
print(f"[警告] '{key}' 不是已知的配置项,已添加")
self.config[key] = value

# 使用示例
# manager = ConfigManager("app_config.json")
# config = manager.load()
# manager.set("port", 9090)
# manager.save()
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 带重试机制的网络请求模拟
"""
实现一个带重试机制的函数,遇到临时性错误时自动重试
"""
import time
import random

class NetworkError(Exception):
"""网络异常"""
pass

class TimeoutError(NetworkError):
"""超时异常"""
pass

class ServerError(NetworkError):
"""服务器异常"""
pass

def simulate_unreliable_request():
"""模拟不稳定的网络请求(随机失败)"""
rand = random.random()
if rand < 0.3:
raise TimeoutError("连接超时")
elif rand < 0.6:
raise ServerError("服务器返回 500 错误")
elif rand < 0.75:
raise NetworkError("网络不可达")
# 剩下 25% 概率成功
return {"status": 200, "data": "请求成功!"}

def request_with_retry(max_retries=3, delay=1):
"""
带重试机制的网络请求

max_retries: 最大重试次数
delay: 每次重试的等待秒数
"""
for attempt in range(1, max_retries + 1):
try:
print(f"第 {attempt} 次尝试...")
result = simulate_unreliable_request()
except TimeoutError as e:
print(f" ⚠ 超时:{e}")
if attempt < max_retries:
print(f" ↩ 等待 {delay} 秒后重试...")
time.sleep(delay)
except ServerError as e:
print(f" ⚠ 服务器错误:{e}")
if attempt < max_retries:
print(f" ↩ 等待 {delay * 2} 秒后重试...") # 服务器错误等久一点
time.sleep(delay * 2)
except NetworkError as e:
print(f" ❌ 网络错误:{e}(不重试)")
return None
else:
print(f" ✅ {result['data']}")
return result["data"]

print(f"❌ 已重试 {max_retries} 次,全部失败")
return None

# 测试(每次运行结果可能不同)
# request_with_retry(max_retries=3, delay=0.5)

异常处理是编写健壮 Python 程序的核心技能。掌握 try-except-else-finally 的完整结构、理解异常的传播机制、善用自定义异常来清晰表达业务错误,以及合理使用断言进行调试——这些能力能让你写出既不容易崩溃、又容易排查问题的代码。记住原则:在知道如何处理的地方捕获异常,不确定时就让异常向上传播。

Python Study Note 系列文章

  1. Python Study Note
  2. Python 函数使用(四)
  3. Python 判断语句(二)
  4. Python 基础语法(一)
  5. Python 异常处理(七)
  6. Python 循环语句(三)
  7. Python 数据容器(五)
  8. Python 文件基础操作(六)
  9. Python 模块与包(八)
  10. Python 类型注解(十)
  11. Python 面向对象(九)重点
  12. Python 高阶技巧(十一)