在 Python 中,异常(Exception) 是程序运行时发生的非预期错误(如除数为 0、访问不存在的列表索引等),会中断程序的正常执行。合理处理异常可以避免程序崩溃,提供友好的错误提示,并确保资源正确释放(如关闭文件、数据库连接)。本文将从异常的基本概念、处理机制、内置异常、自定义异常到最佳实践,全面讲解 Python 异常。

一、异常的基本概念

1. 什么是异常?

异常是程序运行时的 “意外事件”,例如:

  • 试图除以 0(ZeroDivisionError);
  • 访问列表中不存在的索引(IndexError);
  • 打开不存在的文件(FileNotFoundError);
  • 类型不匹配(如用字符串加数字,TypeError)。

如果不处理异常,程序会直接崩溃并抛出错误信息(Traceback):

# 示例:未处理的异常导致程序崩溃
print(10 / 0)  # 运行时触发 ZeroDivisionError
# 输出:
# Traceback (most recent call last):
#   File "test.py", line 1, in <module>
#     print(10 / 0)
# ZeroDivisionError: division by zero

2. 异常与语法错误的区别

  • 语法错误(SyntaxError):代码不符合 Python 语法规则(如缺少冒号、括号不匹配),程序在运行前就会报错,无法执行。

    if True  # 缺少冒号,语法错误
        print("hello")
    # 输出:SyntaxError: expected ':'
    
  • 异常(Exception):代码语法正确,但运行时出现错误(如上述例子),程序可以通过异常处理机制捕获并处理。

二、异常处理的基本机制

Python 用 try-except 语句捕获并处理异常,核心逻辑是:“尝试执行可能出错的代码,若出错则执行预设的处理逻辑”。

1. 基础结构:try-except

try:
    # 可能触发异常的代码块
    risky_code()
except 异常类型1:
    # 若触发异常类型1,执行此代码块
    handle_error1()
except 异常类型2:
    # 若触发异常类型2,执行此代码块
    handle_error2()

示例:处理除以 0 的异常

try:
    num = int(input("请输入一个除数:"))
    result = 10 / num
    print(f"10 / {num} = {result}")
except ZeroDivisionError:
    # 捕获“除数为0”的异常
    print("错误:除数不能为0!")
except ValueError:
    # 捕获“输入无法转为整数”的异常(如输入字母)
    print("错误:请输入有效的整数!")
  • 当 try 块中代码触发 ZeroDivisionError 时,执行第一个 except 块;
  • 若触发 ValueError(如输入 “abc”),执行第二个 except 块;
  • 若未触发任何异常,except 块不执行,程序继续运行。

2. 捕获所有异常:except Exception 或 except

若想捕获所有非系统退出类异常(不建议滥用),可使用 except ExceptionException 是所有内置非系统异常的基类):

try:
    # 可能出错的代码
    10 / 0
except Exception as e:  # 用 as e 捕获异常对象,可获取错误信息
    print(f"发生错误:{e}")  # 输出:发生错误:division by zero

更简单(但更不推荐)的写法是 except:(捕获所有异常,包括系统退出异常如 KeyboardInterrupt):

try:
    10 / 0
except:  # 不推荐:过度捕获,可能隐藏bug
    print("发生了未知错误")

3. else 子句:无异常时执行

else 子句可选,用于定义当 try 块无异常时执行的代码(与 except 互斥):

try:
    num = int(input("请输入一个正数:"))
    if num <= 0:
        raise ValueError("必须输入正数")  # 主动抛异常(见后文)
except ValueError as e:
    print(f"输入错误:{e}")
else:
    # 无异常时执行(num 是正数)
    print(f"你输入的正数是:{num}")

4. finally 子句:无论是否有异常都执行

finally 子句可选,用于定义无论 try 块是否触发异常,都必须执行的代码(通常用于释放资源,如关闭文件、网络连接):

file = None
try:
    file = open("test.txt", "r")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("错误:文件不存在")
finally:
    # 无论是否出错,都关闭文件(释放资源)
    if file:
        file.close()
        print("文件已关闭")

执行顺序

  • 若 try 无异常:try → else → finally
  • 若 try 有异常:try → except → finally

三、Python 内置异常类型

Python 定义了大量内置异常,覆盖各种常见错误场景。以下是最常用的几类:

1. 基础异常类

  • BaseException:所有异常的基类(包括系统退出异常如 SystemExit)。
  • Exception:所有非系统退出异常的基类(日常处理的异常都继承自它)。

2. 常见具体异常

异常类型 触发场景 示例
TypeError 操作或函数应用于不适当类型的对象 1 + "2"(int 加 str)
ValueError 操作或函数接收到的参数类型正确但值无效 int("abc")(字符串无法转为整数)
ZeroDivisionError 除数为 0 10 / 0
IndexError 访问序列(列表、元组等)中不存在的索引 [1,2][3](列表只有 3 个元素,索引 0-2)
KeyError 访问字典中不存在的键 {"name": "Alice"}["age"]
FileNotFoundError 尝试打开不存在的文件(IOError 的子类) open("nonexist.txt")
AttributeError 访问对象不存在的属性 "hello".nonexist_attr
NameError 使用未定义的变量 print(undefined_var)
IndentationError 缩进错误(语法错误的一种) if True: print("hello")(缺少缩进)

示例:触发 KeyError 并处理

user = {"name": "Bob", "age": 20}
try:
    print(user["gender"])  # 访问不存在的键 "gender"
except KeyError as e:
    print(f"错误:键 {e} 不存在")  # 输出:错误:键 'gender' 不存在

3. 异常的继承关系

部分异常存在继承关系,例如:ArithmeticError(算术错误)是 ZeroDivisionErrorOverflowError 等的父类;LookupError(查找错误)是 IndexErrorKeyError 等的父类。

捕获父类异常会同时捕获其子类异常:

try:
    [1,2][3]  # 触发 IndexError(LookupError 的子类)
except LookupError:
    print("捕获到查找错误(IndexError 或 KeyError)")  # 会执行

四、主动抛出异常:raise 语句

除了被动捕获运行时异常,还可以用 raise 语句主动抛出异常,用于在满足特定条件时中断程序并提示错误(如参数校验)。

1. 基本用法:raise 异常类型 或 raise 异常对象

def divide(a, b):
    if b == 0:
        # 主动抛出 ZeroDivisionError
        raise ZeroDivisionError("除数不能为0!")  # 可自定义错误信息
    return a / b

try:
    divide(10, 0)
except ZeroDivisionError as e:
    print(f"捕获到错误:{e}")  # 输出:捕获到错误:除数不能为0!

2. 重新抛出异常:raise

在 except 块中可使用 raise 不带参数,将捕获的异常重新抛出(用于多层异常处理):

try:
    10 / 0
except ZeroDivisionError as e:
    print(f"内层处理:{e},准备向上抛出")
    raise  # 重新抛出异常,让外层处理

运行结果:

内层处理:division by zero,准备向上抛出
Traceback (most recent call last):
  File "test.py", line 2, in <module>
    10 / 0
ZeroDivisionError: division by zero

3. 异常链:raise ... from

用 raise 新异常 from 原异常 可将两个异常关联(原异常是新异常的原因),便于追踪错误根源:

try:
    int("abc")  # 触发 ValueError
except ValueError as original_e:
    # 抛出新异常,并关联原异常
    raise TypeError("转换失败,输入不是数字") from original_e

运行结果(会显示异常链):

Traceback (most recent call last):
  File "test.py", line 2, in <module>
    int("abc")
ValueError: invalid literal for int() with base 10: 'abc'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "test.py", line 5, in <module>
    raise TypeError("转换失败,输入不是数字") from original_e
TypeError: 转换失败,输入不是数字

五、自定义异常

当内置异常无法满足业务需求时(如 “用户年龄必须大于 18 岁”),可通过继承 Exception 类定义自定义异常。

1. 定义自定义异常

# 自定义异常:继承自 Exception
class AgeError(Exception):
    """自定义异常:用于处理年龄不合法的场景"""
    def __init__(self, message):
        self.message = message  # 存储错误信息

    # 可选:自定义异常的字符串表示
    def __str__(self):
        return f"AgeError: {self.message}"

2. 使用自定义异常

def check_age(age):
    if age < 18:
        # 抛出自定义异常
        raise AgeError("年龄必须大于等于18岁")
    print(f"年龄合法:{age}岁")

try:
    check_age(15)
except AgeError as e:
    print(f"捕获到自定义异常:{e}")  # 输出:捕获到自定义异常:AgeError: 年龄必须大于等于18岁

自定义异常通常用于大型项目,可更精确地区分错误类型(如业务错误、参数错误、权限错误等)。

六、异常处理的最佳实践

合理的异常处理能提高程序的健壮性和可维护性,以下是关键原则:

1. 只捕获特定异常,避免过度捕获

不要用 except: 或 except Exception: 捕获所有异常,这会隐藏真正的错误(如拼写错误、逻辑错误)。应精确指定需要处理的异常:

# 推荐:只捕获需要处理的异常
try:
    file = open("data.txt")
except FileNotFoundError:
    print("文件不存在,使用默认数据")
    data = "default"

# 不推荐:过度捕获,可能隐藏其他错误(如变量名拼写错误)
try:
    file = open("data.txt")
except:  # 会捕获所有异常,包括 NameError 等
    print("发生错误")

2. 提供具体的错误信息

异常处理时,应输出清晰的错误信息(如错误原因、影响范围),便于调试:

try:
    user = {"name": "Alice"}
    print(user["age"])
except KeyError as e:
    # 具体说明错误:哪个键不存在,在处理什么数据
    print(f"处理用户数据时出错:键 {e} 不存在,用户信息为 {user}")

3. 用 finally 释放资源

涉及文件、网络连接、数据库连接等资源时,必须在 finally 中释放,确保资源不泄露:

# 示例:用 finally 关闭数据库连接
import sqlite3

conn = None
try:
    conn = sqlite3.connect("mydb.db")
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
except sqlite3.Error as e:
    print(f"数据库错误:{e}")
finally:
    if conn:
        conn.close()  # 无论是否出错,都关闭连接
        print("数据库连接已关闭")

4. 避免在异常处理中忽略错误

不要捕获异常后不做任何处理(“吞掉异常”),这会导致错误无法被发现:

# 不推荐:忽略异常,隐藏问题
try:
    10 / 0
except ZeroDivisionError:
    pass  # 什么都不做,程序看似正常,实则出错

5. 自定义异常用于业务逻辑

在大型项目中,用自定义异常区分业务错误(如 PermissionDeniedErrorResourceNotFoundError),使代码更清晰:

class PermissionDeniedError(Exception):
    """无权限访问时抛出"""

def access_resource(user, resource):
    if user.role != "admin":
        raise PermissionDeniedError(f"用户 {user.name} 无权限访问 {resource}")
    # 正常访问逻辑...

总结

异常是 Python 处理运行时错误的核心机制,通过 try-except-else-finally 结构可实现:

  • 捕获并处理特定异常,避免程序崩溃;
  • 用 else 执行无异常时的逻辑;
  • 用 finally 确保资源释放;
  • 用 raise 主动抛出异常,或定义自定义异常处理业务错误。

遵循 “捕获特定异常、提供清晰信息、释放资源” 等最佳实践,能编写更健壮、易维护的代码