Python 中的错误和异常

6 阅读4分钟

一、错误 vs 异常

概念说明例子
语法错误代码不符合 Python 语法规则,解释器无法解析,程序运行前就会报错if x = 1:(应为 ==)、缺少冒号
逻辑错误语法正确但结果不符合预期,属于 bug,通常不会自动触发异常a / b 中 b 可能为 0 而未处理
异常语法正确,但在运行期间发生错误,导致正常流程中断。Python 会抛出异常对象,可以被捕获处理ZeroDivisionErrorTypeErrorFileNotFoundError

通常我们说的“异常处理”指的是对运行时异常的捕获与恢复,而不是修正语法错误。


二、异常处理基本结构:try / except

try:
    # 可能抛出异常的代码
    value = int(input("输入数字: "))
    result = 10 / value
except ValueError:
    print("请输入有效的整数")
except ZeroDivisionError:
    print("不能除以零")
  • 如果 try 块中发生异常,Python 会跳转到匹配的第一个 except 块。
  • 如果没有匹配的 except,异常会继续向上传递(可能导致程序崩溃)。

捕获所有异常(不推荐,除非日志后重新抛出)

try:
    risky()
except Exception as e:   # 捕获所有非系统退出的异常
    print(f"出错了: {e}")
  • except: 会捕获 BaseException(包括 KeyboardInterruptSystemExit),一般不要这样写。
  • 建议指定具体的异常类型或使用 except Exception as e

三、elsefinally

try:
    file = open("data.txt")
    data = file.read()
except FileNotFoundError:
    print("文件不存在")
else:
    # 仅在 try 块没有发生任何异常时执行
    print(f"文件长度: {len(data)}")
finally:
    # 无论是否发生异常,都会执行(通常用于清理资源)
    if 'file' in locals():
        file.close()
    print("清理完毕")
子句执行时机
else仅当 try 没有抛出异常时执行
finally无论是否发生异常都会执行,即使有 returnbreak

四、手动抛出异常:raise

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError(f"余额不足,需要 {amount},可用 {balance}")
    return balance - amount

可以重新抛出捕获的异常:

try:
    ...
except ValueError as e:
    print("记录日志")
    raise   # 原样抛出,保留调用栈

或抛出不同的异常(异常链):

try:
    ...
except IOError as e:
    raise RuntimeError("读取配置失败") from e

五、自定义异常

通常继承自 Exception 或其子类。

class InsufficientFundsError(Exception):
    """账户余额不足时抛出"""
    pass

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(f"余额不足,需要 {amount},现有 {self.balance}")
        self.balance -= amount

六、常见内置异常

异常触发场景
ZeroDivisionError除法或取模运算中除数为 0
TypeError操作或函数应用于不适当类型的对象(如 1 + "a"
ValueError参数类型正确但值不合适(如 int("abc")
IndexError序列下标越界
KeyError字典中找不到键
AttributeError对象没有某属性
FileNotFoundError打开不存在的文件(Python 3 内置 OSError 子类)
ImportError / ModuleNotFoundError导入模块失败
KeyboardInterrupt用户按 Ctrl+C 中断程序(继承自 BaseException,不是 Exception
StopIteration迭代器没有更多值(内部使用)

七、异常传递与未捕获

如果异常一直向上传递到程序顶层(主线程)仍未被捕获,Python 会打印 traceback 并终止程序。

def a(): b()
def b(): c()
def c(): 1 / 0

a()
# Traceback (most recent call last):
#   File "...", line ..., in <module>
#     a()
#   ...
# ZeroDivisionError: division by zero

八、assert 用于调试断言

assert condition, message 如果条件为假,抛出 AssertionError。通常在开发阶段用于检查不变量,生产环境可用 -O 选项忽略。

def divide(a, b):
    assert b != 0, "除数不能为0"
    return a / b

九、异常处理的最佳实践

  1. 捕获具体异常,不要使用裸露的 except:
  2. 只捕获你能够处理的异常,否则让异常向上传播更合适。
  3. 清理资源使用 finally 或上下文管理器(with 语句)。
  4. 自定义异常使代码语义更清晰。
  5. 记录异常日志,同时可以考虑重新抛出(如果无法处理)。
  6. 不要用异常控制正常流程,异常处理开销较大。

十、示例:带重试的异常处理

import time

def fetch_data(url, retries=3):
    for i in range(retries):
        try:
            response = requests.get(url, timeout=2)
            response.raise_for_status()   # 4xx/5xx 抛出 HTTPError
            return response.json()
        except (requests.ConnectionError, requests.Timeout) as e:
            print(f"网络错误,重试 {i+1}/{retries}")
            time.sleep(1)
        except requests.HTTPError as e:
            print(f"HTTP 错误 {e.response.status_code},不再重试")
            raise   # 不可恢复的错误,直接抛出
    raise Exception(f"无法获取数据,已重试 {retries} 次")

总结

  • 错误:语法错误无法运行;逻辑错误需要修改代码。
  • 异常:运行时的意外情况,可以被捕获并恢复。
  • try/except 捕获异常,else 处理成功情况,finally 执行清理。
  • raise 主动抛出异常,可定义自己的异常类。
  • 编写健壮的程序需要合理使用异常处理,但不要过度使用。