上一篇【第 010 篇】Python模块与包:代码组织最佳实践
下一篇【第 012 篇】Python内置库精讲:os、sys、pathlib、datetime


系列说明:本系列共30篇,涵盖Python开发完整学习路径。本文为第011篇,深入讲解Python异常处理机制。


摘要

在实际的Python开发过程中,程序运行时难免会遇到各种错误:用户输入无效数据、文件不存在、网络连接中断、数组越界访问等。如果这些错误得不到妥善处理,程序就会崩溃退出,用户体验大打折扣。Python提供了完善的异常处理机制,让我们能够优雅地捕获、处理这些运行时错误,确保程序即使遇到问题也能从容应对,而不是直接崩溃。

本文将系统讲解Python异常处理的核心知识:从最基础的try-except语法开始,到elsefinally子句的配合使用,再到自定义异常类和异常的链式传递。


一、异常基础

1.1 什么是异常

在Python中,异常是程序执行过程中发生的事件,它会中断正常的指令流。当Python检测到某个错误时,会创建一个异常对象。

# 除数为零的情况
result = 10 / 0
# ZeroDivisionError: division by zero

# 使用未定义的变量
print(spam)
# NameError: name 'spam' is not defined

语法错误与异常的区别

  • 语法错误(Syntax Errors):代码解析阶段就被发现的错误
  • 异常(Exceptions):代码语法正确,但运行时才发生的错误

1.2 常见异常类型

异常类型 说明 典型场景
ZeroDivisionError 除数为零 10 / 0
NameError 名称未定义 使用不存在的变量
TypeError 类型不匹配 '2' + 2
ValueError 值不合法 int('abc')
IndexError 索引越界 列表只有3个元素却访问第5个
KeyError 键不存在 字典中没有这个键
FileNotFoundError 文件不存在 打开不存在的文件

1.3 异常的错误信息解读

Python的Traceback按从上到下顺序解读:

Traceback (most recent call last):
  File "example.py", line 8, in <module>
    main()
  File "example.py", line 5, in main
    print(names[10])
IndexError: list index out of range
  1. 最外层:程序执行的起点
  2. 调用链的上一级:函数调用关系
  3. 异常类型和消息:告诉我们发生了什么问题

二、try-except 语法

2.1 基本语法结构

def safe_divide(a, b):
    """安全的除法运算"""
    try:
        result = a / b
        print(f"计算结果: {result}")
    except ZeroDivisionError:
        print("错误: 除数不能为零!")

safe_divide(10, 2)    # 计算结果: 5.0
safe_divide(10, 0)    # 错误: 除数不能为零!

2.2 捕获特定异常

def process_user_input():
    """处理用户输入"""
    try:
        age = int(input("请输入您的年龄: "))
        print(f"您的年龄是: {age}")
    except ValueError:
        print("错误: 请输入有效的整数!")

2.3 多个except子句

def read_file(filename):
    """读取文件内容"""
    try:
        with open(filename, 'r', encoding='utf-8') as file:
            content = file.read()
            print(f"文件内容长度: {len(content)} 字符")
    except FileNotFoundError:
        print(f"错误: 文件 '{filename}' 不存在!")
    except PermissionError:
        print(f"错误: 没有权限读取文件 '{filename}'!")

2.4 捕获异常对象

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"异常类型: {type(e).__name__}")
    print(f"异常消息: {e}")

2.5 异常处理顺序

应该先捕获子类,再捕获父类

try:
    risky_code()
except SpecificError:
    # 先处理具体的异常
    handle_specific()
except BaseError:
    # 再处理更通用的异常
    handle_base()

三、else与finally子句

3.1 else的用途

else块中的代码会在try成功执行且没有发生异常时运行:

def parse_number(text):
    """解析数字"""
    try:
        number = int(text)
    except ValueError:
        print(f"'{text}' 不是有效的数字")
    else:
        # 只有在成功解析时才执行
        print(f"成功解析数字: {number}")
        return number

parse_number("42")    # 成功解析数字: 42
parse_number("hello")  # 'hello' 不是有效的数字

3.2 finally的用途

无论try块中是否发生异常,finally子句中的代码总是会执行

def demonstrate_finally():
    """演示finally的作用"""
    try:
        result = 10 / 0
    except ZeroDivisionError:
        print("捕获到 ZeroDivisionError")
    finally:
        print("finally块总是执行")

3.3 资源清理

finally最常见的用途是资源清理:

def copy_file(source_path, dest_path):
    """复制文件"""
    source_file = None
    dest_file = None
    
    try:
        source_file = open(source_path, 'r', encoding='utf-8')
        dest_file = open(dest_path, 'w', encoding='utf-8')
        content = source_file.read()
        dest_file.write(content)
    except FileNotFoundError as e:
        print(f"错误: {e}")
    finally:
        if source_file is not None:
            source_file.close()
        if dest_file is not None:
            dest_file.close()

不过,更好的做法是使用with语句(上下文管理器)。


四、抛出异常

4.1 raise语句

def validate_age(age):
    """验证年龄是否合法"""
    if age < 0:
        raise ValueError("年龄不能为负数")
    if age > 150:
        raise ValueError("年龄值不合理")
    return True

try:
    validate_age(-5)
except ValueError as e:
    print(f"验证失败: {e}")

raise语句的几种形式

# 1. 抛出异常对象
raise ValueError("错误信息")

# 2. 重新抛出当前捕获的异常
except Exception:
    raise

# 3. 重新抛出指定异常
except SomeError:
    raise AnotherError("新的错误") from None

4.2 自定义异常类

class ValidationError(Exception):
    """数据验证错误"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")


class EmailValidator:
    @staticmethod
    def validate(email):
        if '@' not in email:
            raise ValidationError('email', '邮箱格式不正确')
        return True


try:
    EmailValidator.validate("not-an-email")
except ValidationError as e:
    print(f"验证错误 - 字段: {e.field}, 原因: {e.message}")

4.3 异常的链式传递

隐式异常链

def read_config(filename):
    try:
        with open(filename, 'r') as f:
            return f.read()
    except FileNotFoundError:
        raise RuntimeError(f"无法读取配置文件: {filename}")

显式异常链(使用from):

try:
    result = int("not a number")
except ValueError as original:
    raise RuntimeError("数据处理失败") from original

五、异常处理最佳实践

5.1 异常捕获原则

原则一:尽可能捕获具体异常

# 推荐 - 针对具体异常
try:
    result = risky_calculation()
except ZeroDivisionError:
    print("除数不能为零")
except ValueError as e:
    print(f"数值错误: {e}")

原则二:让异常传播

不要过度捕获异常。如果当前代码无法合理处理某个异常,应该让它传播到上层调用者。

5.2 避免裸except

# ❌ 不推荐
try:
    result = dangerous_operation()
except:
    print("出错了")

# ✅ 推荐
try:
    result = dangerous_operation()
except Exception as e:
    print(f"操作失败: {e}")

5.3 异常日志记录

import logging

logging.basicConfig(
    level=logging.ERROR,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def process_data(data):
    try:
        return validate_and_transform(data)
    except ValueError as e:
        logger.error(f"数据验证失败: %s", data, exc_info=True)
        raise

5.4 异常与用户输入验证

def get_positive_number():
    """获取正数"""
    while True:
        try:
            number = int(input("请输入一个正数: "))
            if number <= 0:
                print("错误: 必须输入正数")
                continue
            return number
        except ValueError:
            print("错误: 请输入有效的整数")

六、实战:健壮的程序设计

6.1 异常处理模式

重试机制

import time

def retry_with_backoff(func, max_retries=3, initial_delay=1):
    """带退避的重试机制"""
    delay = initial_delay
    
    for attempt in range(max_retries):
        try:
            return func()
        except (ConnectionError, TimeoutError) as e:
            if attempt == max_retries - 1:
                raise
            print(f"第{attempt + 1}次尝试失败,{delay}秒后重试...")
            time.sleep(delay)
            delay *= 2

6.2 上下文管理器

with语句确保资源在使用后被正确释放:

# 文件操作
with open('example.txt', 'r', encoding='utf-8') as file:
    content = file.read()
# 文件自动关闭

# 自定义上下文管理器
class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start
        print(f"耗时 {elapsed:.2f} 秒")
        return False

with Timer() as t:
    data = list(range(100000))
    data.sort()

6.3 断言的使用

断言用于在开发阶段检查程序状态:

def calculate_average(numbers):
    """计算平均值"""
    assert len(numbers) > 0, "数字列表不能为空"
    return sum(numbers) / len(numbers)

# 断言与异常的区别
def factorial(n):
    assert n >= 0, "n必须是非负整数"  # 开发阶段检查
    if n == 0:
        return 1
    return n * factorial(n - 1)

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为零")  # 用户输入需要运行时检查
    return a / b

七、常见问题与注意事项

Q1: 异常处理会影响性能吗?

正常情况下,异常处理几乎没有性能影响。只有在异常真的发生时,才会有少量额外开销。

Q2: 什么时候应该捕获异常?

  • 当你知道如何处理这个异常时
  • 当你需要执行某些清理操作时
  • 当你想要提供更友好的错误信息时

Q3: 什么时候应该让异常传播?

  • 当当前代码无法合理处理这个异常时
  • 当这个异常应该终止程序时(如配置错误)

八、总结

本文详细介绍了Python异常处理的核心知识:

  1. 异常基础:异常是程序运行时发生的错误
  2. try-except语法:捕获并处理特定异常
  3. else与finallyelse块在成功时运行,finally块总是执行
  4. 抛出异常:使用raise语句主动抛出异常
  5. 最佳实践:尽量捕获具体异常,使用日志记录异常

上一篇【第 010 篇】Python模块与包:代码组织最佳实践
下一篇【第 012 篇】Python内置库精讲:os、sys、pathlib、datetime


参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐