在Python编程中,“异常”是不可避免的——文件不存在、网络中断、数据类型不匹配、索引越界等场景,都可能导致程序意外崩溃。如果不进行异常处理,不仅会影响用户体验,还可能导致数据丢失、资源泄露等严重问题。

异常处理的核心不是“隐藏错误”,而是“优雅地处理错误”,让程序在遇到异常时能够正常降级、保留上下文信息,同时方便开发者排查问题。本文将从**基础语法、进阶用法、实战场景、最佳实践**四个维度,全面讲解Python异常捕获的方式,帮你写出更健壮、更易维护的Python代码。

一、先搞懂:Python中的异常类型

在学习捕获异常之前,我们先明确Python中常见的异常类型,这是精准捕获异常的基础。

异常类型

说明

常见场景

SyntaxError

语法错误

代码缩进错误、括号不匹配、关键字拼写错误(不属于运行时异常,无法通过try-except捕获)

NameError

名称错误

引用未定义的变量、函数名拼写错误

TypeError

类型错误

不同类型数据进行无效操作(如字符串+数字)

ValueError

值错误

数据类型正确但值无效(如int("abc")

FileNotFoundError

文件未找到错误

读取不存在的文件

IndexError

索引越界错误

访问列表/元组不存在的索引

KeyError

键错误

访问字典不存在的键

AttributeError

属性错误

访问对象不存在的属性/方法

IOError

IO错误

文件读写失败(如权限不足)

requests.exceptions.RequestException

网络请求错误

(第三方库)网络断开、请求超时、状态码异常

注意:SyntaxError是语法错误,在程序运行前就会被解释器检测到,无法通过异常处理机制捕获;其他异常均为运行时异常,是我们处理的重点。

二、基础语法:Python异常捕获的核心结构

Python异常处理的核心是try-except语句,配合elsefinallyraise,构成完整的异常处理体系。

1. 最基础:try-except(捕获指定异常)

这是异常处理的最小单元,用于捕获并处理指定类型的异常。

语法结构

try:
    # 可能抛出异常的代码块(业务逻辑)
    risky_code()
except 异常类型1:
    # 捕获到异常类型1时执行的逻辑
    handle_exception1()
except 异常类型2:
    # 捕获到异常类型2时执行的逻辑
    handle_exception2()

代码示例
# 示例1:捕获单个异常
def convert_to_int(s):
    try:
        return int(s)
    except ValueError:
        print(f"错误:'{s}' 无法转换为整数")
        return None

# 测试
print(convert_to_int("123"))  # 正常执行:123
print(convert_to_int("abc"))  # 捕获异常:错误:'abc' 无法转换为整数 → None

# 示例2:捕获多个异常
def calculate(a, b, op):
    try:
        if op == "+":
            return a + b
        elif op == "/":
            return a / b
        else:
            raise ValueError(f"不支持的操作:{op}")
    except TypeError:
        print("错误:a和b必须是数字类型")
        return None
    except ZeroDivisionError:
        print("错误:除数不能为0")
        return None

# 测试
print(calculate(10, 2, "+"))  # 12
print(calculate(10, 0, "/"))  # 错误:除数不能为0 → None
print(calculate(10, "2", "+"))  # 错误:a和b必须是数字类型 → None

2. 捕获所有异常:except(裸except)

使用不带异常类型的except可以捕获所有运行时异常,适合快速调试或兜底处理。

语法示例
def risky_operation():
    try:
        # 复杂业务逻辑,可能抛出多种异常
        lst = [1,2,3]
        print(lst[10])  # 索引越界
        print("abc" + 123)  # 类型错误
    except:  # 捕获所有运行时异常
        print("发生了未知异常,程序已降级处理")
        return False
    return True

# 测试
risky_operation()  # 输出:发生了未知异常,程序已降级处理

警告:生产环境中尽量避免使用“裸except”!它会捕获所有异常(包括KeyboardInterrupt(用户中断)、SystemExit(程序退出)等),可能隐藏严重问题,且难以排查根因。

3. 增强版:try-except-else

else子句用于执行“当try代码块无异常时”的逻辑,将“正常业务”与“异常处理”分离,代码更清晰。

语法示例
def read_file_content(file_path):
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
    except FileNotFoundError:
        print(f"错误:文件 {file_path} 不存在")
        return ""
    else:
        # 仅当try块无异常时执行
        print(f"成功读取文件 {file_path},内容长度:{len(content)}")
        return content

# 测试
read_file_content("test.txt")  # 若文件存在,执行else;不存在则执行except

4. 必执行:try-except-finally

finally子句中的代码**无论****try****块是否抛出异常,都会执行**,常用于释放资源(如关闭文件、数据库连接、网络连接等)。

语法示例
# 示例1:手动关闭文件(不使用with语句时)
def read_file_manual(file_path):
    f = None
    try:
        f = open(file_path, "r", encoding="utf-8")
        content = f.read()
        print(f"文件内容:{content}")
        return content
    except FileNotFoundError:
        print(f"错误:文件 {file_path} 不存在")
        return ""
    finally:
        # 无论是否异常,都关闭文件
        if f is not None:
            f.close()
            print("文件已关闭")

# 测试
read_file_manual("test.txt")

# 示例2:模拟释放数据库连接
def connect_db():
    print("建立数据库连接")
    return "db_connection"

def close_db(conn):
    print("关闭数据库连接")

def query_db(sql):
    conn = None
    try:
        conn = connect_db()
        print(f"执行SQL:{sql}")
        # 模拟查询异常
        if "error" in sql:
            raise IOError("数据库查询失败")
        return "查询结果"
    except IOError as e:
        print(f"错误:{e}")
        return None
    finally:
        if conn is not None:
            close_db(conn)

# 测试
query_db("SELECT * FROM user")  # 正常执行:建立连接→执行SQL→关闭连接
query_db("SELECT * FROM error")  # 异常执行:建立连接→抛出异常→关闭连接

提示:Python中的with语句(上下文管理器)是try-finally的语法糖,可自动释放资源(如文件操作),优先使用with而非手动finally关闭资源。

5. 主动抛异常:raise语句

除了Python内置异常,我们还可以通过raise主动抛出异常(内置异常或自定义异常),用于业务逻辑的异常提示(如参数校验失败、业务规则违规等)。

语法示例
# 示例1:抛出内置异常
def check_age(age):
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数类型")
    if age < 0 or age > 150:
        raise ValueError(f"年龄 {age} 无效,必须在0-150之间")
    print(f"年龄 {age} 验证通过")

# 测试
try:
    check_age(200)  # 抛出ValueError
except (TypeError, ValueError) as e:
    print(f"错误:{e}")

# 示例2:抛出异常并附带详细信息
def transfer_money(from_account, to_account, amount):
    if amount <= 0:
        raise ValueError("转账金额必须大于0", {"from": from_account, "to": to_account, "amount": amount})
    # 模拟转账逻辑
    print(f"成功从 {from_account} 转账 {amount} 到 {to_account}")

# 测试
try:
    transfer_money("A123", "B456", -100)
except ValueError as e:
    print(f"转账失败:{e.args[0]}")
    print(f"异常详情:{e.args[1]}")

三、进阶用法:提升异常处理的灵活性

1. 捕获异常的详细信息:as关键字

使用except 异常类型 as e可以捕获异常实例,获取异常的详细信息(如错误描述、堆栈信息等),方便排查问题。

import traceback

def test_exception_detail():
    try:
        lst = [1,2,3]
        print(lst[10])
    except IndexError as e:
        # 1. 获取错误描述
        print(f"错误描述:{e}")
        print(f"错误类型:{type(e).__name__}")
        # 2. 获取完整堆栈信息(便于调试)
        print("\n完整堆栈信息:")
        traceback.print_exc()  # 打印堆栈信息到控制台
        # 3. 将堆栈信息写入日志文件
        with open("error.log", "a", encoding="utf-8") as f:
            traceback.print_exc(file=f)

# 测试
test_exception_detail()

2. 自定义异常:满足业务个性化需求

Python内置异常无法覆盖所有业务场景(如“余额不足”“用户不存在”等),此时可以自定义异常类,继承自Exception(推荐)或BaseException

语法示例
# 1. 定义自定义异常(继承自Exception)
class BusinessException(Exception):
    """业务异常基类"""
    def __init__(self, code, message):
        self.code = code  # 错误码
        self.message = message  # 错误信息
        super().__init__(f"[{code}] {message}")

# 2. 定义具体业务异常
class InsufficientBalanceException(BusinessException):
    """余额不足异常"""
    def __init__(self, account, balance, need_amount):
        self.account = account
        self.balance = balance
        self.need_amount = need_amount
        super().__init__(
            code=1001,
            message=f"账户 {account} 余额不足:当前 {balance},需要 {need_amount}"
        )

class UserNotExistException(BusinessException):
    """用户不存在异常"""
    def __init__(self, user_id):
        self.user_id = user_id
        super().__init__(
            code=1002,
            message=f"用户 {user_id} 不存在"
        )

# 3. 使用自定义异常
def deduct_balance(user_id, amount):
    # 模拟用户查询
    user_map = {"U001": 1000, "U002": 500}
    if user_id not in user_map:
        raise UserNotExistException(user_id)
    balance = user_map[user_id]
    if balance < amount:
        raise InsufficientBalanceException(user_id, balance, amount)
    # 模拟扣款
    user_map[user_id] -= amount
    print(f"用户 {user_id} 扣款成功,剩余余额:{user_map[user_id]}")

# 4. 捕获自定义异常
try:
    deduct_balance("U002", 1000)
except BusinessException as e:
    print(f"业务异常:{e}")
    print(f"错误码:{e.code}")
    # 针对具体异常做特殊处理
    if isinstance(e, InsufficientBalanceException):
        print(f"补充提示:请为账户 {e.account} 充值")

3. 异常链:保留原始异常上下文

Python 3.3+ 支持raise 新异常 from 原始异常,用于构建异常链,保留原始异常的上下文信息,便于排查嵌套异常。

def parse_json(json_str):
    import json
    try:
        data = json.loads(json_str)
    except json.JSONDecodeError as e:
        # 抛出业务异常,并保留原始异常上下文
        raise BusinessException(2001, "JSON解析失败") from e
    return data

# 测试
try:
    parse_json("{name: '张三'}")  # 无效JSON
except BusinessException as e:
    print(f"错误:{e}")
    # 打印原始异常
    print(f"原始异常:{e.__cause__}")

四、实战场景:异常处理的实际应用

场景1:文件操作中的异常处理

文件操作是异常高发场景(文件不存在、权限不足、编码错误等),需针对性捕获异常。

def file_operation_demo(file_path):
    try:
        # 读取文件
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
        # 写入新文件
        with open(f"{file_path}.bak", "w", encoding="utf-8") as f:
            f.write(content)
        print(f"成功备份文件 {file_path} 到 {file_path}.bak")
    except FileNotFoundError:
        print(f"错误:文件 {file_path} 不存在")
    except PermissionError:
        print(f"错误:没有权限读写文件 {file_path}")
    except UnicodeDecodeError:
        print(f"错误:文件 {file_path} 编码不是utf-8")
    except Exception as e:
        print(f"未知错误:{e}")
    else:
        return True
    return False

# 测试
file_operation_demo("test.txt")

场景2:网络请求中的异常处理

使用requests库进行网络请求时,需捕获网络异常、超时异常、状态码异常等。

import requests
from requests.exceptions import RequestException, ConnectTimeout, HTTPError

def http_request_demo(url, timeout=5):
    try:
        response = requests.get(url, timeout=timeout)
        # 主动抛出状态码异常(4xx/5xx)
        response.raise_for_status()
        return response.json()
    except ConnectTimeout:
        print(f"错误:连接 {url} 超时({timeout}秒)")
    except HTTPError as e:
        print(f"错误:请求 {url} 失败,状态码:{e.response.status_code}")
    except RequestException as e:
        print(f"网络请求错误:{e}")
    except ValueError:
        print(f"错误:{url} 返回的不是有效JSON")
    return None

# 测试
http_request_demo("https://api.github.com/users/github")  # 正常请求
http_request_demo("https://api.github.com/invalid", timeout=2)  # 404异常

场景3:数据处理中的异常处理

数据转换、索引访问、字典键访问等场景,需处理类型错误、值错误、索引错误等。

def data_process_demo(data_list):
    result = []
    for idx, item in enumerate(data_list):
        try:
            # 数据转换
            id = int(item.get("id"))
            name = str(item.get("name")).strip()
            age = int(item.get("age", 0))
            # 数据校验
            if not name:
                raise ValueError("姓名不能为空")
            result.append({"id": id, "name": name, "age": age})
        except KeyError as e:
            print(f"第 {idx} 条数据错误:缺少字段 {e}")
        except (TypeError, ValueError) as e:
            print(f"第 {idx} 条数据错误:{e}")
        except Exception as e:
            print(f"第 {idx} 条数据未知错误:{e}")
    return result

# 测试数据
test_data = [
    {"id": "1", "name": "张三", "age": "20"},
    {"id": "2", "name": "", "age": "25"},  # 姓名为空
    {"id": "abc", "name": "李四"},  # id无法转整数
    {"name": "王五", "age": "30"}  # 缺少id字段
]

# 执行
print(data_process_demo(test_data))

场景4:函数参数校验的异常处理

使用异常处理进行函数参数校验,比手动if判断更简洁,且能统一异常处理逻辑。

def add(a, b):
    """加法函数,仅支持数字类型"""
    try:
        return float(a) + float(b)
    except (TypeError, ValueError) as e:
        raise TypeError(f"参数a和b必须是数字类型:{e}") from e

# 测试
try:
    print(add(10, 20))
    print(add(10, "abc"))
except TypeError as e:
    print(f"错误:{e}")

五、最佳实践与避坑指南

1. 最佳实践

(1)优先捕获指定异常,避免裸except
# 不好的写法
try:
    risky_code()
except:
    pass  # 隐藏所有异常,难以排查

# 好的写法
try:
    risky_code()
except (FileNotFoundError, ValueError) as e:
    print(f"处理已知异常:{e}")
except Exception as e:
    print(f"记录未知异常:{e}")
    raise  # 重新抛出异常,不隐藏问题

(2)异常粒度适中,避免过度拆分
# 不好的写法(过度拆分,代码冗余)
try:
    a = int(x)
except ValueError:
    print("x无法转整数")
try:
    b = int(y)
except ValueError:
    print("y无法转整数")

# 好的写法(合理合并,代码简洁)
try:
    a = int(x)
    b = int(y)
except ValueError as e:
    print(f"数据转换失败:{e}")

(3)使用with语句自动释放资源
# 不好的写法(手动关闭资源,容易遗漏)
f = open("test.txt", "r")
try:
    content = f.read()
finally:
    f.close()

# 好的写法(with自动关闭资源)
with open("test.txt", "r", encoding="utf-8") as f:
    content = f.read()

(4)异常信息要明确,便于排查
# 不好的写法(信息模糊)
except ValueError:
    raise ValueError("数据错误")

# 好的写法(信息明确,包含上下文)
except ValueError:
    raise ValueError(f"用户ID {user_id} 转换失败,原始值:{raw_id}")

(5)自定义异常继承Exception而非BaseException

BaseException包含SystemExitKeyboardInterrupt等系统异常,自定义异常继承Exception可避免捕获系统异常。

2. 避坑指南

(1)不要捕获异常后不处理(“吞掉”异常)
# 危险写法:捕获异常后无任何处理,隐藏问题
try:
    db.insert(data)
except DatabaseError:
    pass  # 数据插入失败但无提示,导致业务数据不一致

(2)不要在finally中返回值

finally中的return会覆盖try/except中的返回值,导致异常信息丢失。

# 危险写法
def test_finally_return():
    try:
        1 / 0
        return "正常返回"
    except ZeroDivisionError:
        return "异常返回"
    finally:
        return "finally返回"  # 会覆盖上面的返回值

# 测试
print(test_finally_return())  # 输出:finally返回

(3)不要滥用异常处理替代正常判断
# 不好的写法(用异常处理替代正常判断)
def get_user_name(user_list, user_id):
    try:
        return next(u for u in user_list if u["id"] == user_id)["name"]
    except StopIteration:
        return None

# 好的写法(正常判断,更高效)
def get_user_name(user_list, user_id):
    for u in user_list:
        if u["id"] == user_id:
            return u["name"]
    return None

六、总结

Python异常处理是编写健壮程序的必备技能,其核心要点可总结为:

  1. 基础结构try-except捕获异常,else执行正常逻辑,finally释放资源,raise主动抛异常;

  2. 精准捕获:优先捕获指定异常,避免裸except,使用as关键字获取异常详情;

  3. 进阶用法:自定义异常满足业务需求,异常链保留原始上下文,traceback打印堆栈信息;

  4. 实战原则:针对文件、网络、数据等场景做针对性处理,遵循“明确信息、自动释放、不吞异常”的最佳实践;

  5. 避坑要点:不滥用异常、不隐藏异常、不在finally中返回值、不替代正常判断。

异常处理的终极目标,是让程序在面对意外时能够“优雅降级”,同时为开发者提供足够的上下文信息来排查问题。掌握本文的内容,你可以在实际项目中写出更稳定、更易维护的Python代码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐