Skip to content

🛡 异常处理 ​

程序运行中总会遇到各种意外——文件不存在、网络断了、用户输入了奇怪的东西。异常处理就是给程序穿上铠甲。

异常是什么? ​

异常(Exception)是程序运行时抛出的错误信号。不处理的后果:程序崩溃,后面代码全不跑。

python
print("程序开始")
result = 10 / 0    # ZeroDivisionError!程序在此崩溃
print("这行不会执行")

try/except — 捕获异常 ​

python
try:
    num = int(input("请输入一个数字:"))
    print(f"100 ÷ {num} = {100 / num}")
except ValueError:
    print("不是数字!请重新运行。")
except ZeroDivisionError:
    print("不能除以零!")

else 和 finally ​

python
def read_config(path):
    config = None
    try:
        with open(path, "r", encoding="utf-8") as f:
            config = f.read()
    except FileNotFoundError:
        print(f"配置文件 {path} 不存在,使用默认配置")
        config = "default"
    else:
        # try 块没出异常才执行
        print(f"成功读取配置文件({len(config)} 字符)")
    finally:
        # 无论如何都执行(清理资源)
        print("配置加载流程结束")
    return config
  • try:可能出问题的代码
  • except:出问题后怎么处理
  • else:没出问题才跑(可选)
  • finally:不管出不出问题都要跑(可选,常用于释放资源)

常见异常类型 ​

异常触发条件示例
ValueError类型对但值不对int("abc")
TypeError类型不对"a" + 1
KeyError字典键不存在d["不存在的键"]
IndexError列表索引越界[1,2][5]
FileNotFoundError文件不存在open("不存在.txt")
ZeroDivisionError除以零1 / 0
AttributeError对象没有这个属性"hi".fake_method()
ImportError导入失败import 不存在的模块
OSError系统操作失败权限不足、磁盘满

完整继承关系:BaseException → Exception → 具体异常。日常捕获用 Exception 即可。

raise — 主动抛异常 ​

python
def set_age(age):
    if age < 0:
        raise ValueError(f"年龄不能为负数:{age}")
    if age > 150:
        raise ValueError(f"年龄不太可能超过150:{age}")
    print(f"年龄设置为 {age}")

set_age(25)   # OK
# set_age(-5)  # 抛异常

自定义异常 ​

python
class BalanceError(Exception):
    """余额不足异常"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"余额 {balance} 不足,无法取款 {amount}")

def withdraw(balance, amount):
    if amount > balance:
        raise BalanceError(balance, amount)
    return balance - amount

try:
    withdraw(100, 200)
except BalanceError as e:
    print(e)  # 余额 100 不足,无法取款 200

异常链 — from ​

一个异常引发另一个异常时,保留因果关系:

python
def load_data(path):
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"数据文件 {path} 加载失败") from e

# 抛出 RuntimeError,但用 __cause__ 指向原始 FileNotFoundError

用 from None 可以切断因果链:raise ValueError("数据无效") from None。

捕获的哲学 ​

什么时候捕获异常?

捕获异常的目的是恢复或优雅退出,不是隐藏 bug。

  • ✅ 该捕获的:文件可能不存在、网络可能断、用户输入不可控
  • ❌ 不该捕获的:代码逻辑错误(类型错误、索引越界),这些应该修代码而不是藏起来

最坏的写法

python
# ❌ 什么都吞掉,出了问题你永远不知道
try:
    do_something()
except:
    pass  # 什么也不做

出 bug 时你看到的是"程序莫名其妙不 work 了",排查到吐血。

实战:健壮的文件读写 + 重试机制 ​

python
# /opt/kb/docs/python/exceptions_demo.py
"""实战:健壮的文件读写、API调用重试"""
import time
import json
from pathlib import Path

def safe_read_json(path, default=None):
    """安全读取 JSON,文件不存在或格式错误时返回默认值"""
    try:
        return json.loads(Path(path).read_text(encoding="utf-8"))
    except FileNotFoundError:
        print(f"[警告] {path} 不存在,使用默认值")
    except json.JSONDecodeError as e:
        print(f"[错误] {path} JSON 格式错误:{e}")
    except Exception as e:
        print(f"[错误] 读取 {path} 时发生意外:{e}")
    return default if default is not None else {}

def retry_call(func, max_retries=3, delay=1):
    """带重试的函数调用(适合网络请求等不稳定操作)"""
    for attempt in range(1, max_retries + 1):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries:
                raise  # 最后一次也失败了,抛出异常
            print(f"第 {attempt} 次失败:{e},{delay}秒后重试...")
            time.sleep(delay)

# 模拟不稳定的 API
import random
def unstable_api():
    """模拟:30% 概率失败的 API"""
    if random.random() < 0.3:
        raise ConnectionError("网络超时")
    return {"data": "success"}

# 使用
if __name__ == "__main__":
    # 1. 安全读取
    config = safe_read_json("config.json", {"theme": "light"})
    print(f"配置: {config}")

    # 2. 带重试
    try:
        result = retry_call(unstable_api, max_retries=5, delay=0.5)
        print(f"API 返回: {result}")
    except Exception as e:
        print(f"API 最终失败: {e}")

常见坑 ​

坑1:except 太宽

python
# ❌ 捕获所有异常,包括 KeyboardInterrupt(Ctrl+C)和 SystemExit
try:
    risky_work()
except Exception:  # 至少用 Exception,别裸写 except:
    pass            # 但什么都不做也很糟糕

至少记录日志:except Exception as e: logger.error(f"操作失败: {e}")。

坑2:except 后面什么都不做

except: pass 是"灭迹模式"——出错了你永远不知道。至少 print() 或 logging.error()。

坑3:finally 里 return

python
def bad():
    try:
        return "try"
    finally:
        return "finally"  # 覆盖了 try 的返回值!
print(bad())  # "finally" — try 的 return 被吞了

finally 里的 return 会吞掉 try/except 中的 return。不要在 finally 里写 return。

练习题 ​

  1. 写程序让用户输入数字做除法,捕获 ValueError 和 ZeroDivisionError,反复提示直到输入正确
  2. 自定义一个 InvalidPasswordError 异常,写一个密码校验函数,要求至少 8 位且含数字
  3. 模拟读取 10 个文件(有的存在有的不存在),用异常处理跳过不存在的
  4. 实现一个 safe_divide(a, b) 函数,除以零时返回 None 而不是崩溃
  5. 给 retry_call 加指数退避(delay 翻倍):第1次等1秒、第2次等2秒、第3次等4秒

🛡 异常处理让程序坚不可摧。下节学 常用标准库,摸清 Python 自带的宝库。


🎯 本章要点 ​

  • else 只在 try 块没有抛出任何异常时才执行。
  • finally 无论是否发生异常都会执行
  • FileNotFoundError 是 Python 内置异常。
加载练习题中...