017、Python异常处理:让程序更健壮(try/except)
017、Python异常处理:让程序更健壮(try/except)
昨天调一个嵌入式设备的日志解析脚本,遇到个典型问题:脚本从串口读数据,解析完写入数据库。跑了两小时突然崩了,控制台就一行 ValueError: invalid literal for int() with base 10: 'N/A'。查日志发现设备偶尔会返回 N/A 表示传感器异常,而我的转换函数 int() 直接把这字符串扔进去了。这种问题在生产环境太常见——外部数据不可靠,网络会断,文件会缺失,用户会乱输入。今天我们就聊聊怎么用 try/except 让程序扛得住意外。
为什么需要异常处理?
写代码时我们总假设一切按理想状态运行:文件肯定存在、用户输入都是数字、网络永远通畅。但真实世界充满意外,这些意外在程序里就是“异常”。如果不处理,程序会直接崩溃退出。异常处理的核心思想是:预见可能出错的地方,给程序一个恢复的机会。
比如刚才那个例子,设备返回 N/A 时,我们更希望记录一条警告日志,跳过这条数据,而不是让整个解析任务中断。这就是异常处理的价值。
基础用法:try/except 块
先看最简单的形式:
try:
# 可能出错的代码放这里
value = int(user_input)
print(f"转换结果: {value}")
except ValueError:
# 如果发生 ValueError 就执行这里
print("输入的不是有效数字,请重试")
这个结构把“正常流程”和“错误处理”分开。如果 int() 转换失败,Python 会跳转到 except 块,打印提示后程序继续往下走。注意这里只捕获了 ValueError,其他异常(比如 KeyboardInterrupt)还是会中断程序——这是好事,避免隐藏真正的问题。
捕获多种异常
实际场景中,一段代码可能抛出多种异常。比如读文件:
try:
with open("config.json", "r") as f:
data = json.load(f)
timeout = int(data["timeout"])
except FileNotFoundError:
print("配置文件丢了,用默认设置")
timeout = 30
except (KeyError, TypeError):
print("配置文件格式不对,检查 timeout 字段")
timeout = 30
except json.JSONDecodeError as e:
print(f"JSON 解析失败: {e}")
timeout = 30
这里有几个细节:
- 可以用多个
except块处理不同类型的异常 - 用元组
(KeyError, TypeError)一次捕获多种异常 - 用
as e把异常对象赋值给变量,方便打印错误信息
别这样写:有人图省事直接 except Exception:,这会捕获所有异常,连 Ctrl+C 都拦截,调试时很痛苦。应该明确指定能处理的异常类型。
else 和 finally:常被忽略的好帮手
完整的结构其实有四部分:
try:
result = risky_operation()
except SomeError:
handle_error()
else:
# 如果没发生异常,执行这里
print(f"操作成功,结果是: {result}")
save_to_database(result)
finally:
# 无论是否异常,最后都执行这里
cleanup_resources()
else 块只在没有异常时执行,适合放那些依赖 try 块成功结果的代码。如果把 save_to_database(result) 放在 try 块末尾,万一它自己抛出异常,会被前面的 except 捕获,导致错误处理逻辑混乱。用 else 可以避免这种耦合。
finally 最适合做清理工作:关闭文件、释放锁、断开网络连接。这里踩过坑——之前写设备通信脚本,在 try 里打开串口,异常时直接 return 了,结果串口没关闭,下次打开就报错。后来所有资源操作都放在 finally 里,再没出过问题。
异常对象和自定义异常
异常不只是个错误类型,它携带信息:
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.exceptions.Timeout:
print("请求超时,可能是网络慢")
except requests.exceptions.HTTPError as e:
print(f"服务器返回错误状态码: {e.response.status_code}")
if e.response.status_code == 404:
print("资源不存在")
有时候内置异常不够用,可以自定义:
class SensorError(Exception):
"""传感器数据异常"""
def __init__(self, sensor_id, message):
self.sensor_id = sensor_id
self.message = message
super().__init__(f"[Sensor {sensor_id}] {message}")
def read_sensor(sensor_id):
raw = read_adc()
if raw == 0xFFFF:
raise SensorError(sensor_id, "ADC 读取超量程")
return raw * 0.1
自定义异常能让错误分类更清晰,调用方可以针对性地处理:
try:
temperature = read_sensor(1)
except SensorError as e:
logger.warning(f"传感器异常: {e}")
temperature = estimate_from_other_sensors()
几个实际场景的写法
场景一:处理用户输入,直到输入合法
while True:
try:
age = int(input("请输入年龄: "))
if not 0 < age < 150:
raise ValueError("年龄不合理") # 主动抛出异常
break
except ValueError as e:
print(f"输入无效: {e}")
这里用 raise 主动抛异常,把数据验证也纳入异常处理流程。
场景二:数据库操作,失败时回滚
def save_user_data(user):
conn = get_db_connection()
try:
conn.execute("BEGIN")
conn.execute("INSERT INTO users ...", user)
conn.execute("INSERT INTO logs ...", user)
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise # 重新抛出异常,让上层知道失败了
finally:
conn.close()
注意 raise 不带参数表示重新抛出当前异常,这样既做了回滚,又不掩盖错误。
场景三:嵌入式设备读取,容忍偶尔失败
def read_temperature_with_retry(max_retries=3):
for attempt in range(max_retries):
try:
return i2c_read(0x48, 0x00)
except OSError as e: # I2C 通信错误常见
if attempt == max_retries - 1:
raise
time.sleep(0.1 * (2 ** attempt)) # 指数退避
raise RuntimeError("重试多次仍失败")
这种带重试的异常处理在硬件通信中很实用,硬件偶尔丢一两个包是正常的。
个人经验与建议
-
异常不是错误:它是程序流程的一部分。设计时就该想清楚“这里可能出什么岔子,出了怎么办”,而不是事后补
try/except。 -
保持异常处理局部化:在可能出错的地方就近处理,别把一堆代码包在一个大
try块里。否则很难判断到底是哪行出的问题。 -
日志比 print 有用:生产环境看不到控制台,该用
logging.error(e, exc_info=True)记录完整堆栈。 -
别用异常做常规控制流:比如遍历列表时用
try/except StopIteration代替for循环,这种“炫技”写法既慢又难读。 -
嵌入式场景注意资源:单片机内存小,异常处理会占用额外空间。如果异常路径极少执行(如硬件故障),可以考虑用错误码代替异常,但要做好文档。
-
向上传递该传递的:底层函数不知道如何处理异常时,就让它抛出来。比如解析函数遇到格式错误,通常应该抛出让调用者决定是重试、跳过还是报错。
最后留个习惯:写完异常处理代码后,问自己两个问题:第一,如果这里真的出错了,程序能继续提供核心功能吗?第二,调试时看到这个异常信息,我能立刻定位问题吗?这两个问题能帮你写出更健壮的代码。
下次我们聊聊 with 语句和上下文管理器——它和异常处理配合,能让资源管理代码更简洁。
更多推荐


所有评论(0)