aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Python-第20篇《异常处理:让程序更健壮》

导语

“容错”的重要性!——报销单写错金额要退回重填,报表数据异常要标红提醒。程序也一样,用户输入“abc”当工资、除数为零、文件找不到…如果不管这些,程序就会崩溃罢工。今天咱们给代码装上“安全气囊”,让它遇到错误能优雅处理,继续运行。毕竟,一个成熟的程序,应该学会自己处理异常!


本篇目标

  • 理解错误和异常的区别
  • 掌握try/except捕获错误
  • 学会数据验证和安全输入
  • 构建能防“手滑”的财务计算器
  • 让程序从“一错就挂”到“百折不挠”

一、准备工作:无需安装

异常处理是Python内置功能,不需要额外安装库,直接开干:

1
2
3
4
# 纯Python,无需安装
import json
from datetime import datetime
import os

二、错误 vs 异常:先搞清楚概念

2.1 语法错误(Syntax Error)

就像报销单格式写错了,系统直接拒收。

1
2
3
4
5
# 这是语法错误,程序根本跑不起来
# print("hello" # 缺少右括号

# if a > b # 缺少冒号
# print("a更大")

特点:代码写错了,Python解释器直接报错,程序无法运行。


2.2 运行时异常(Exception)

就像报销单格式对了,但金额写成了“三万”而不是数字,系统能识别但处理不了。

1
2
3
4
5
6
7
8
9
10
# 这是运行时异常,程序跑到这里才出问题

# 1. 除零错误
# result = 100 / 0 # ZeroDivisionError: division by zero

# 2. 类型错误
# "工资: " + 15000 # TypeError: can only concatenate str (not "int") to str

# 3. 值错误
# int("abc") # ValueError: invalid literal for int() with base 10: 'abc'

特点:语法没问题,但运行时逻辑出错,程序会在这里崩溃。


三、try/except:给代码装上“安全气囊”

3.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ========== 基础异常捕获 ==========
def safe_divide(a, b):
"""
安全的除法函数
财务场景:计算人均报销金额
"""
try:
result = a / b
print(f"✅ 计算成功:{a} / {b} = {result}")
return result
except ZeroDivisionError:
print("❌ 错误:除数不能为零!人均不能是0人")
return 0
except Exception as e:
print(f"❌ 未知错误: {e}")
return None

# 测试
print("人均报销计算测试:")
safe_divide(10000, 5) # 正常情况
safe_divide(10000, 0) # 除零错误
safe_divide(10000, "abc") # 类型错误,会触发未知错误

3.2 捕获多个异常

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
# ========== 多异常捕获 ==========
def calculate_bonus(sales_amount: str, bonus_rate: str):
"""
计算销售提成
能处理各种输入错误
"""
try:
# 尝试转换输入为数字
sales = float(sales_amount)
rate = float(bonus_rate)

if sales < 0:
raise ValueError("销售额不能为负数") # 主动抛出异常

if rate < 0 or rate > 0.5:
raise ValueError("提成比例必须在0-50%之间")

bonus = sales * rate
print(f"✅ 提成计算成功:¥{bonus:,.2f}")
return bonus

except ValueError as e:
print(f"❌ 输入数据错误: {e}")
return 0
except TypeError as e:
print(f"❌ 类型错误: {e}")
return 0
except Exception as e:
print(f"❌ 意外错误: {e}")
return 0

# 测试
print("\n提成计算测试:")
calculate_bonus("50000", "0.08") # 正常
calculate_bonus("-1000", "0.05") # 负销售额
calculate_bonus("abc", "0.05") # 非数字
calculate_bonus("50000", "0.8") # 提成比例过高

四、完整异常结构:try/except/else/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
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
# ========== 完整异常处理结构 ==========
def process_salary_file(filepath):
"""
处理工资文件:读取、计算、保存
完整展示try结构
"""
print(f"\n📂 开始处理文件: {filepath}")

try:
# 1. try块:可能出错的代码
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)

# 2. 数据处理(如果上面没出错才会执行到这里)
total_salary = sum([emp['salary'] for emp in data['employees']])
print(f"✅ 数据加载成功,员工数: {len(data['employees'])}")

except FileNotFoundError:
# 3. except:捕获特定错误
print("❌ 错误:文件不存在!请检查文件路径")
return False

except json.JSONDecodeError:
# JSON格式错误
print("❌ 错误:文件格式错误!不是有效的JSON文件")
return False

except KeyError as e:
# 缺少必要字段
print(f"❌ 错误:数据格式不正确,缺少字段 {e}")
return False

except Exception as e:
# 其他所有错误
print(f"❌ 未知错误: {e}")
return False

else:
# 4. else:try块没有发生异常时执行
print(f"🎉 数据处理成功!工资总额: ¥{total_salary:,.2f}")
return True

finally:
# 5. finally:无论是否出错都会执行(清理工作)
print(f"⏰ 处理完成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

# 测试完整结构
# 创建测试文件
test_data = {
"employees": [
{"name": "张三", "salary": 15000},
{"name": "李四", "salary": 12000}
]
}

with open('工资数据.json', 'w', encoding='utf-8') as f:
json.dump(test_data, f, ensure_ascii=False, indent=2)

# 正常处理
process_salary_file('工资数据.json')

# 错误情况
process_salary_file('不存在的文件.json')
process_salary_file('工资数据.json.bak') # 创建个错误文件测试

五、数据验证:提前预防错误

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
80
81
82
83
84
85
86
87
88
89
90
# ========== 财务数据验证器 ==========
class FinanceValidator:
"""财务数据验证工具"""

@staticmethod
def validate_amount(amount_str: str, min_value: float = 0, max_value: float = 1000000) -> float:
"""
验证金额输入
返回:验证通过返回金额,不通过抛出异常
"""
try:
amount = float(amount_str)
except ValueError:
raise ValueError(f"❌ 金额必须是数字,不能是'{amount_str}'")

if amount < min_value:
raise ValueError(f"❌ 金额不能小于{min_value},当前值: {amount}")

if amount > max_value:
raise ValueError(f"❌ 金额不能超过{max_value},当前值: {amount}")

# 检查小数位数(财务通常最多2位)
if round(amount, 2) != amount:
raise ValueError(f"❌ 金额最多保留2位小数,当前值: {amount}")

return amount

@staticmethod
def validate_date(date_str: str) -> str:
"""验证日期格式"""
try:
datetime.strptime(date_str, '%Y-%m-%d')
return date_str
except ValueError:
raise ValueError(f"❌ 日期格式错误,应为YYYY-MM-DD,不能是'{date_str}'")

@staticmethod
def validate_rate(rate_str: str) -> float:
"""验证比率(如税率、提成)"""
try:
rate = float(rate_str)
except ValueError:
raise ValueError(f"❌ 比率必须是数字,不能是'{rate_str}'")

if rate < 0 or rate > 1:
raise ValueError(f"❌ 比率必须在0-1之间(0%到100%),当前值: {rate}")

return rate

@staticmethod
def validate_employee_id(emp_id: str) -> str:
"""验证员工工号格式"""
emp_id = emp_id.strip()

if not emp_id:
raise ValueError("❌ 工号不能为空")

if len(emp_id) > 10:
raise ValueError(f"❌ 工号不能超过10位,当前长度: {len(emp_id)}")

if not emp_id[0].isalpha():
raise ValueError(f"❌ 工号首位必须是字母,当前值: {emp_id}")

return emp_id

# 测试验证器
print("\n财务数据验证测试:")
validator = FinanceValidator()

try:
validator.validate_amount("15000.55") # 正常
print("✅ 金额验证通过")
except Exception as e:
print(e)

try:
validator.validate_amount("-500") # 负数
except Exception as e:
print(e)

try:
validator.validate_rate("0.15") # 正常15%
print("✅ 比率验证通过")
except Exception as e:
print(e)

try:
validator.validate_rate("150%") # 格式错误
except Exception as e:
print(e)

六、实战:安全的财务计算器

6.1 设计思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ========== 安全财务计算器设计 ==========
"""
功能需求:
1. 计算税后工资(容错:负数、非数字、超出范围)
2. 计算报销总额(容错:文件不存在、格式错误)
3. 计算项目ROI(容错:除零、数据缺失)
4. 所有操作记录日志,便于审计

安全策略:
- 输入验证:提前拦截非法数据
- 异常捕获:运行时错误不崩溃
- 日志记录:所有操作可追溯
- 默认值:错误时返回安全值(0)
"""

6.2 完整实现

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# ========== 安全财务计算器 - 完整版 ==========
import json
import os
from datetime import datetime
from typing import List, Dict

class SafeFinanceCalculator:
"""安全的财务计算器"""

def __init__(self, log_file="finance_calc.log"):
self.log_file = log_file
self.validator = FinanceValidator()

def log(self, operation: str, details: str, status: str = "SUCCESS"):
"""记录操作日志"""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_entry = f"[{timestamp}] [{status}] {operation}: {details}\n"

try:
with open(self.log_file, 'a', encoding='utf-8') as f:
f.write(log_entry)
except Exception as e:
# 日志写入失败也不影响主功能
print(f"⚠️ 日志写入失败: {e}")

def calculate_tax_salary(self, gross_salary: str, insurance_str: str = "0") -> float:
"""
计算税后工资(安全版)
返回:税后工资(出错返回0)
"""
try:
# 1. 验证输入
salary = self.validator.validate_amount(gross_salary, min_value=0, max_value=100000)
insurance = self.validator.validate_amount(insurance_str, min_value=0, max_value=50000)

# 2. 计算税后工资
taxable = salary - insurance - 5000

if taxable <= 0:
tax = 0
elif taxable <= 36000:
tax = taxable * 0.03
else:
tax = taxable * 0.10 - 2520

net_salary = salary - insurance - tax

# 3. 记录成功日志
self.log("工资计算", f"税前{salary},五险一金{insurance},税后{net_salary:.2f}")

return round(net_salary, 2)

except Exception as e:
# 4. 错误处理 + 记录日志
error_msg = str(e)
self.log("工资计算", f"输入-gross:{gross_salary},insurance:{insurance_str},错误:{error_msg}", "ERROR")
print(f"❌ 计算失败: {error_msg},已返回默认值0")
return 0

def reimburse_from_file(self, filepath: str) -> float:
"""
从文件读取报销明细并计算总额
返回:报销总额(出错返回0)
"""
try:
# 1. 检查文件是否存在
if not os.path.exists(filepath):
raise FileNotFoundError(f"文件'{filepath}'不存在")

# 2. 读取并解析
try:
with open(filepath, 'r', encoding='utf-8') as f:
reimburse_data = json.load(f)
except json.JSONDecodeError:
raise ValueError(f"文件'{filepath}'不是有效的JSON格式")

# 3. 验证数据结构
if 'reimbursements' not in reimburse_data:
raise KeyError("缺少'reimbursements'字段")

reimbursements = reimburse_data['reimbursements']

if not isinstance(reimbursements, list):
raise TypeError("'reimbursements'必须是列表")

# 4. 计算总额
total = 0
errors = []

for i, item in enumerate(reimbursements, 1):
try:
# 验证每条记录
if 'amount' not in item:
raise KeyError(f"第{i}条记录缺少'amount'字段")

amount = self.validator.validate_amount(
str(item['amount']),
min_value=0,
max_value=50000
)

# 验证审批状态
status = item.get('status', 'pending')
if status != 'approved':
print(f"⚠️ 第{i}条记录状态为'{status}',跳过计算")
continue

total += amount

except Exception as e:
errors.append(f"第{i}条: {e}")
continue

# 5. 记录日志
if errors:
self.log("报销总额计算", f"成功{len(reimbursements)-len(errors)}条,失败{len(errors)}条", "WARNING")
for err in errors:
self.log("报销明细错误", err, "ERROR")
else:
self.log("报销总额计算", f"文件{filepath},总额{total:.2f},记录数{len(reimbursements)}")

return round(total, 2)

except Exception as e:
# 顶层异常捕获
self.log("报销总额计算", f"文件{filepath},错误:{str(e)}", "ERROR")
print(f"❌ 计算失败: {e}")
return 0

def calculate_roi(self, investment: str, profit: str) -> float:
"""
计算ROI投资回报率
ROI = (收益 - 成本) / 成本 * 100%
返回:ROI百分比(出错返回0)
"""
try:
invest = self.validator.validate_amount(investment, min_value=0.01, max_value=10000000)
prof = self.validator.validate_amount(profit, min_value=0, max_value=10000000)

# 计算ROI
roi = (prof - invest) / invest * 100

# 记录日志
self.log("ROI计算", f"投资{invest},收益{prof},ROI:{roi:.2f}%")

return round(roi, 2)

except Exception as e:
self.log("ROI计算", f"输入-inv:{investment},profit:{profit},错误:{str(e)}", "ERROR")
print(f"❌ 计算失败: {e}")
return 0

def show_log(self, lines: int = 10):
"""查看最近的操作日志"""
try:
if not os.path.exists(self.log_file):
print("📭 暂无日志")
return

print(f"\n📋 最近 {lines} 条操作日志:")
print("=" * 70)

with open(self.log_file, 'r', encoding='utf-8') as f:
all_logs = f.readlines()

# 显示最后几行
for log in all_logs[-lines:]:
print(log.strip())

except Exception as e:
print(f"❌ 读取日志失败: {e}")

def main():
"""主菜单"""
calculator = SafeFinanceCalculator()

print("=" * 55)
print("安全财务计算器 v1.0")
print("所有操作均有日志记录,错误自动处理")
print("=" * 55)

while True:
print("\n" + "=" * 50)
print("功能菜单")
print("=" * 50)
print("1. 计算税后工资")
print("2. 计算报销总额(从文件)")
print("3. 计算项目ROI")
print("4. 查看操作日志")
print("5. 退出")
print("=" * 50)

choice = input("请选择: ").strip()

if choice == "1":
salary = input("税前工资: ").strip()
insurance = input("五险一金(回车默认为0): ").strip() or "0"
result = calculator.calculate_tax_salary(salary, insurance)
if result > 0:
print(f"✅ 税后工资: ¥{result:,.2f}")
else:
print("⚠️ 计算失败,请检查输入")

elif choice == "2":
filepath = input("报销文件路径: ").strip()
if not filepath:
filepath = "reimbursements.json"

# 如果不存在,创建示例文件
if not os.path.exists(filepath):
print("⚠️ 文件不存在,创建示例文件...")
sample_data = {
"reimbursements": [
{"amount": 500, "status": "approved", "category": "交通费"},
{"amount": 1200, "status": "approved", "category": "餐饮费"},
{"amount": 800, "status": "pending", "category": "办公用品"}
]
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(sample_data, f, ensure_ascii=False, indent=2)
print(f"✅ 示例文件'{filepath}'已创建")

result = calculator.reimburse_from_file(filepath)
if result > 0:
print(f"✅ 可报销总额: ¥{result:,.2f}")
else:
print("⚠️ 计算失败或无可报销项目")

elif choice == "3":
investment = input("项目投资成本: ").strip()
profit = input("项目收益: ").strip()
result = calculator.calculate_roi(investment, profit)
if result > 0:
print(f"✅ 投资回报率: {result:.2f}%")
else:
print(f"ℹ️ 投资回报率: {result:.2f}%")

elif choice == "4":
lines = input("显示最近多少条日志(默认10): ").strip() or "10"
try:
calculator.show_log(int(lines))
except:
calculator.show_log()

elif choice == "5":
print("👋 感谢使用,日志已保存至 finance_calc.log")
break

else:
print("请输入1-5之间的数字!")

if __name__ == "__main__":
main()

七、知识点加油站

7.1 常见异常类型速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""
常见异常类型:

FileNotFoundError 文件不存在
PermissionError 无权限访问
IOError 输入输出错误

ValueError 值错误(类型对但值不对)
TypeError 类型错误(字符串+数字)
ZeroDivisionError 除零错误

KeyError 字典键不存在
IndexError 列表索引越界
AttributeError 对象没有该属性

json.JSONDecodeError JSON解析失败
ImportError 导入模块失败
NameError 变量未定义

万能捕获:
except Exception as e: # 捕获所有异常
"""

7.2 try/except最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 不好的做法
try:
result = a / b
except: # 捕获所有异常,但不处理
pass # 静默忽略,问题被隐藏

# ✅ 好的做法
try:
result = a / b
except ZeroDivisionError:
print("除数为零")
result = 0
except Exception as e:
print(f"未知错误: {e}")
result = 0

7.3 何时使用异常处理

场景处理方式示例
用户输入必须 验证+异常处理输入金额、日期
文件操作必须 异常处理读取/写入文件
网络请求必须 异常处理API调用、网页抓取
数据转换必须 异常处理字符串转数字
内部计算可选 验证+异常处理除法运算
确定安全的操作不需要len(list)for i in range()

八、总结

  • ✅ 理解错误和异常的区别
  • ✅ try/except捕获和处理异常
  • ✅ else/finally完整结构
  • ✅ 数据验证的防御性编程
  • ✅ 构建带日志的安全计算器

下篇预告

第21篇《正则表达式:文本处理利器》——快速从混乱文本中提取金额、日期、账号,财务数据清洗必备神器!


🤖 Powered by Kimi K2 Thinking 💻 内容经葵葵🌻审核与修改