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("重试多次仍失败")

这种带重试的异常处理在硬件通信中很实用,硬件偶尔丢一两个包是正常的。


个人经验与建议

  1. 异常不是错误:它是程序流程的一部分。设计时就该想清楚“这里可能出什么岔子,出了怎么办”,而不是事后补 try/except

  2. 保持异常处理局部化:在可能出错的地方就近处理,别把一堆代码包在一个大 try 块里。否则很难判断到底是哪行出的问题。

  3. 日志比 print 有用:生产环境看不到控制台,该用 logging.error(e, exc_info=True) 记录完整堆栈。

  4. 别用异常做常规控制流:比如遍历列表时用 try/except StopIteration 代替 for 循环,这种“炫技”写法既慢又难读。

  5. 嵌入式场景注意资源:单片机内存小,异常处理会占用额外空间。如果异常路径极少执行(如硬件故障),可以考虑用错误码代替异常,但要做好文档。

  6. 向上传递该传递的:底层函数不知道如何处理异常时,就让它抛出来。比如解析函数遇到格式错误,通常应该抛出让调用者决定是重试、跳过还是报错。

最后留个习惯:写完异常处理代码后,问自己两个问题:第一,如果这里真的出错了,程序能继续提供核心功能吗?第二,调试时看到这个异常信息,我能立刻定位问题吗?这两个问题能帮你写出更健壮的代码。


下次我们聊聊 with 语句和上下文管理器——它和异常处理配合,能让资源管理代码更简洁。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐