Python 错误处理:程序的"应急预案"
引言:从"硬撑"到"优雅处理"
想象你开车:
- 硬撑(错误码):仪表盘亮红灯,你猜可能是发动机问题、可能是轮胎问题、可能是没油了——每开一段就要停车检查,效率极低;
- 应急预案(try...except):车一报警,系统自动识别故障类型,告诉你"左后轮爆胎",同时启动备用方案——问题定位精准,处理及时,车还能继续开。
Python 的 try...except...finally 就是程序的"应急预案系统"。
一、为什么不用错误码?
1.1 错误码的困境
def foo():
r = some_function()
if r == -1: # 错误码混在返回值里
return -1 # 一级级往上抛
return r
def bar():
r = foo()
if r == -1:
print('Error')
else:
pass问题:
- 正常结果和错误码混在一起,调用者必须写大量
if判断; - 出错要一级级上报,代码臃肿;
- 不同错误用不同数字,记不住、易混淆。
1.2 异常机制的优势
def foo():
return some_function() # 出错自动抛异常,不用管
def bar():
foo() # 也不用管
def main():
try:
bar()
except SomeError as e: # 在合适的地方统一处理
print('出错了:', e)优势:错误自动冒泡,在能处理的层级捕获,代码简洁。
二、try...except...finally:三段式应急处理
2.1 基本结构
try:
print('try...')
r = 10 / 0 # 这里会出错
print('result:', r) # 不会执行
except ZeroDivisionError as e:
print('except:', e) # 处理错误
finally:
print('finally...') # 无论是否出错,都会执行
print('END')输出:
try...
except: division by zero
finally...
END生活化理解:
try:正常开车上路;except:爆胎了,换备胎;finally:无论是否爆胎,到目的地后都要洗车(必须做的事)。
2.2 没有错误时
try:
r = 10 / 2 # 正常
except ZeroDivisionError as e:
print('except:', e) # 不执行
else:
print('no error!') # 执行
finally:
print('finally...') # 执行else 的作用:没有异常时执行,与 except 互斥。
2.3 捕获多种错误
try:
r = 10 / int('a') # int('a') 会抛 ValueError
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')注意顺序:子类异常要在父类之前捕获,否则会被父类"截胡":
try:
foo()
except ValueError as e: # UnicodeError 是 ValueError 的子类
print('ValueError')
except UnicodeError as e: # ❌ 永远执行不到!
print('UnicodeError')修正:子类在前,父类在后:
except UnicodeError as e: # ✅ 先捕获子类
print('UnicodeError')
except ValueError as e: # 再捕获父类
print('ValueError')2.4 跨层级捕获
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0') # foo 出错,bar 不处理,main 统一捕获
except Exception as e:
print('Error:', e)
main()核心思想:不需要每层都 try,在能处理的层级捕获即可。
三、调用栈:错误的"行车记录仪"
3.1 错误信息长什么样?
# err.py
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
bar('0')
main()运行:
Traceback (most recent call last):
File "err.py", line 11, in <module>
main()
File "err.py", line 9, in main
bar('0')
File "err.py", line 6, in bar
return foo(s) * 2
File "err.py", line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero3.2 如何解读?
从下往上读(从最近调用到源头):
| 层级 | 文件 | 行号 | 代码 | 分析 |
|---|---|---|---|---|
| 最顶层 | err.py | 11 | main() | 程序入口 |
| ↓ | err.py | 9 | bar('0') | main 调 bar |
| ↓ | err.py | 6 | foo(s) * 2 | bar 调 foo |
| 源头 | err.py | 3 | 10 / int(s) | 错误发生在这里 |
最后一行 ZeroDivisionError: division by zero 是错误类型和原因。
生活化理解:调用栈像快递物流信息——从发货地到收货地,每个中转站都有记录,最后一行是"包裹破损"的原因。
3.3 核心原则
出错不可怕,可怕的是不知道哪里出错。一定要分析调用栈,定位错误源头。
四、记录错误:程序的"黑匣子"
4.1 logging 记录错误
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e) # 记录完整错误堆栈
main()
print('END') # 程序继续执行!输出:
ERROR:root:division by zero
Traceback (most recent call last):
File "err.py", line 13, in main
bar('0')
...
ZeroDivisionError: division by zero
END优势:
- 错误被记录,但程序不中断;
- 可以配置输出到文件,方便事后排查;
- 比
print更专业,支持级别(DEBUG/INFO/WARNING/ERROR)。
五、抛出错误:主动报告问题
5.1 为什么要主动抛异常?
def foo(s):
n = int(s)
if n == 0:
raise ValueError('invalid value: %s' % s) # 主动报告问题
return 10 / n场景:参数不合法、业务规则违反、外部服务不可用——提前暴露问题,而不是让错误蔓延。
5.2 自定义错误类型
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid value: %s' % s)
return 10 / n原则:优先用内置类型(ValueError、TypeError),不够用时才自定义。
5.3 raise 的两种用法
用法 1:原样抛出(记录后继续上报)
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!') # 记录一下
raise # 原样抛给上层处理用法 2:转换错误类型
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!') # 转换成更合适的类型生活化理解:员工遇到客户投诉(ZeroDivisionError),记录后上报给经理(raise),经理根据情况转成"服务流程问题"(ValueError)给 CEO——转换要合理,不能把"产品质量"说成"物流延迟"。
六、知识链条:错误处理完整流程
代码执行出错
↓
自动抛出异常(或主动 raise)
↓
异常沿调用栈向上冒泡
↓
某层级 try...except 捕获
↓
处理(记录日志/转换类型/恢复操作)
↓
finally 执行清理工作
↓
程序继续运行或优雅退出七、常见误区与避坑指南
7.1 误区一:捕获所有异常却不处理
try:
risky_operation()
except: # ❌ 捕获所有,包括 KeyboardInterrupt
pass # ❌ 静默吞掉,问题被掩盖修正:明确捕获类型,或至少记录:
except SpecificError as e:
logging.error(e)
raise7.2 误区二:在 finally 里写 return
def foo():
try:
return 1
finally:
return 2 # ❌ 覆盖 try 里的 return!finally 里的 return 会覆盖前面的返回值,慎用。
7.3 误区三:异常处理嵌套过深
try:
try:
try:
...
except A:
...
except B:
...
except C:
...修正:扁平化,在统一层级处理。
7.4 误区四:用异常控制正常流程
# ❌ 用异常代替 if-else
try:
value = my_dict['key']
except KeyError:
value = 'default'
# ✅ 更好的方式
value = my_dict.get('key', 'default')原则:异常用于意外情况,不是常规逻辑。
八、实际应用案例
案例 1:文件下载器
import logging
import requests
logging.basicConfig(level=logging.ERROR)
def download_file(url, save_path):
"""下载文件,带错误处理和重试"""
max_retries = 3
for attempt in range(max_retries):
try:
print(f'尝试下载 (第 {attempt + 1} 次)...')
response = requests.get(url, timeout=10)
response.raise_for_status() # 4xx/5xx 抛异常
with open(save_path, 'wb') as f:
f.write(response.content)
print(f'下载成功: {save_path}')
return True
except requests.exceptions.Timeout:
print('下载超时')
if attempt == max_retries - 1:
logging.error(f'下载失败,超时: {url}')
return False
except requests.exceptions.HTTPError as e:
print(f'HTTP 错误: {e.response.status_code}')
logging.error(f'下载失败,HTTP {e.response.status_code}: {url}')
return False # 客户端错误不重试
except Exception as e:
logging.exception(f'未知错误: {url}')
return False
# 使用
download_file('https://example.com/file.zip', 'file.zip')
download_file('https://example.com/not-found', 'not-found.zip')生活化理解:下载器像快递员——超时(收件人不在家)就再试一次,地址错误(404)就直接上报,交通意外(未知错误)就记录后放弃。
案例 2:用户注册验证
class ValidationError(ValueError):
"""验证错误基类"""
pass
class UsernameTooShortError(ValidationError):
pass
class PasswordTooWeakError(ValidationError):
pass
class DuplicateUserError(ValidationError):
pass
def validate_username(username):
if len(username) < 3:
raise UsernameTooShortError('用户名至少3个字符')
def validate_password(password):
if len(password) < 6:
raise PasswordTooWeakError('密码至少6位')
if password.isdigit():
raise PasswordTooWeakError('密码不能全是数字')
def check_duplicate(username):
# 模拟数据库查询
existing_users = ['admin', 'root', 'test']
if username in existing_users:
raise DuplicateUserError(f'用户名 {username} 已被占用')
def register(username, password):
"""用户注册"""
try:
validate_username(username)
validate_password(password)
check_duplicate(username)
# 保存到数据库...
print(f'注册成功: {username}')
return True
except ValidationError as e:
print(f'注册失败: {e}')
return False
# 测试
register('ab', '123456') # 注册失败: 用户名至少3个字符
register('alice', '123') # 注册失败: 密码至少6位
register('admin', 'abc123') # 注册失败: 用户名 admin 已被占用
register('bob', 'abc123') # 注册成功: bob生活化理解:注册像机场安检——每道关卡(用户名、密码、重复检查)独立,任何一道不通过就明确告知原因,不让问题混过去。
九、实战练习
练习:修复代码错误
运行下面的代码,根据异常信息定位并修复错误:
from functools import reduce
def str2num(s):
return int(s)
def calc(exp):
ss = exp.split('+')
ns = map(str2num, ss)
return reduce(lambda acc, x: acc + x, ns)
def main():
r = calc('100 + 200 + 345')
print('100 + 200 + 345 =', r)
r = calc('99 + 88 + 7.6')
print('99 + 88 + 7.6 =', r)
main()参考答案
错误分析:'99 + 88 + 7.6' 中 '7.6' 是浮点数,int('7.6') 会抛 ValueError。
修复:str2num 支持浮点数:
from functools import reduce
def str2num(s):
s = s.strip() # 去除空格
if '.' in s:
return float(s)
return int(s)
def calc(exp):
ss = exp.split('+')
ns = map(str2num, ss)
return reduce(lambda acc, x: acc + x, ns)
def main():
r = calc('100 + 200 + 345')
print('100 + 200 + 345 =', r)
r = calc('99 + 88 + 7.6')
print('99 + 88 + 7.6 =', r)
main()十、小结
- 错误码 vs 异常:错误码混在返回值里,异常自动冒泡、精准定位;
- 三段式:
try执行代码,except处理错误,finally必须清理; - 捕获顺序:子类在前,父类在后,避免被"截胡";
- 调用栈:从下往上读,最后一行是错误源头,分析调用栈是定位错误的关键;
- 记录错误:
logging.exception(e)记录堆栈,程序继续运行; - 抛出错误:
raise主动报告问题,可原样抛出或转换类型; - 使用原则:异常用于意外情况,不是常规流程控制;在能处理的层级捕获,不要每层都 try。
错误处理是程序的"应急预案"——平时感觉不到它的存在,一旦出事,它能救命。