跳过正文
Python 错误与异常
  1. Blog/

Python 错误与异常

·2055 字·5 分钟
目录
Python - 这篇文章属于一个选集。
§ 7: 本文

在任何编程语言中,错误都是不可避免的。Python 提供了一套强大而灵活的异常处理机制,它不仅能帮助我们捕获和处理运行时错误,更是控制程序流程、构建可靠应用的重要工具。

错误类型:语法错误与异常
#

语法错误 (Syntax Errors)
#

语法错误,也称为解析错误,是程序在被解释器解析阶段就发现的错误。这意味着代码甚至还没开始运行。通常是因为违反了 Python 的语法规则,比如拼写错误的关键字、缺少冒号或括号不匹配。

while True print('Hello world')
#   File "<stdin>", line 1
#     while True print('Hello world')
#                ^^^^^
# SyntaxError: invalid syntax

解释器会指出出错的行,并用一个小箭头 ^ 标记出最早检测到错误的位置。这类错误必须在运行前修复。

异常 (Exceptions)
#

即使程序的语法完全正确,在运行期间也可能发生错误,这些错误被称为异常。当 Python 解释器遇到一个它无法处理的情况时,就会“抛出 (raise)”一个异常。

常见的内置异常包括:

  • ZeroDivisionError: 尝试除以零。
  • NameError: 使用一个未被定义的局部或全局变量。
  • TypeError: 将一个操作或函数应用于了不合适类型的对象。
  • ValueError: 操作或函数的参数类型正确,但值不合适。
  • FileNotFoundError: 尝试打开一个不存在的文件。
  • KeyError: 在字典中查找一个不存在的键。

当异常发生且未被处理时,程序会立即中止,并打印出一条回溯 (Traceback) 信息,它显示了异常发生时的完整调用栈。

捕获与处理异常
#

为了防止异常导致程序崩溃,我们可以使用 try...except 语句来捕获并处理它们。

  • try 子句: 包含可能会引发异常的代码块。
  • except 子句: 如果 try 子句中发生了指定类型的异常,解释器会立即跳转到对应的 except 子句执行。

捕获特定异常
#

这是最推荐的做法。只捕获你明确知道如何处理的特定异常。

try:
    x = int(input("Please enter a number: "))
    result = 10 / x
    print(f"10 / {x} = {result}")
except ValueError:
    print("Oops! That was not a valid number. Try again...")
except ZeroDivisionError:
    print("Oops! You cannot divide by zero.")

你可以为一个 try 块提供多个 except 子句来处理不同类型的异常。

捕获多个异常及对象
#

如果多个异常的处理逻辑相同,可以将它们放在一个元组里。你还可以通过 as 关键字来访问异常对象本身,它通常包含了错误的详细信息。

try:
    # ... some code ...
except (RuntimeError, TypeError, NameError) as err:
    print(f"An error occurred: {err}")
    # err 是异常类的实例,打印它通常会显示错误消息

elsefinally 执行逻辑
#

try 语句可以与 elsefinally 子句配合使用,以实现更复杂的控制流和清理逻辑。理解它们的执行时机至关重要。

核心定义
#

  • else 子句:它的存在是为了**“庆祝成功”。它只在 try 块没有引发任何异常**的情况下执行。
  • finally 子句:它的存在是为了**“无论如何都要清理”。总是**会执行,不管 try 块中是否发生了异常,也不管异常是否被 except 捕获。

场景化执行流程分析
#

让我们用一个统一的函数来分析所有可能的情况:

def test_division(dividend, divisor):
    print(f"\\n--- Testing {dividend} / {divisor} ---")
    try:
        print("Step 1: Entering the 'try' block.")
        result = dividend / divisor
        print("Step 2: Division successful.")
    except ZeroDivisionError:
        print("Step 3 (Except): Caught a ZeroDivisionError!")
    else:
        print(f"Step 4 (Else): No exceptions occurred! The result is {result}.")
    finally:
        print("Step 5 (Finally): This cleanup block always runs.")
  • 场景 1: 成功执行(无异常)

    执行路径: try -> else -> finally

    test_division(10, 2)
    

    输出:

    --- Testing 10 / 2 ---
    Step 1: Entering the 'try' block.
    Step 2: Division successful.
    Step 4 (Else): No exceptions occurred! The result is 5.0.
    Step 5 (Finally): This cleanup block always runs.
    

    分析: try 块成功执行,因此跳过 except 并执行 else,最后执行 finally

  • 场景 2: 发生异常并被捕获

    执行路径: try (失败) -> except -> finally

    test_division(10, 0)
    

    输出:

    --- Testing 10 / 0 ---
    Step 1: Entering the 'try' block.
    Step 3 (Except): Caught a ZeroDivisionError!
    Step 5 (Finally): This cleanup block always runs.
    

    分析: try 块中发生异常,立即跳转到匹配的 except 块。else 块被跳过,最后执行 finally

  • 场景 3: 发生异常但未被捕获

    执行路径: try (失败) -> finally -> 异常向上传播

    test_division(10, 'a') # 这会引发 TypeError
    

    输出及行为:

    --- Testing 10 / 'a' ---
    Step 1: Entering the 'try' block.
    Step 5 (Finally): This cleanup block always runs.
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in test_division
    TypeError: unsupported operand type(s) for /: 'int' and 'str'
    

    分析: try 块中发生 TypeError,没有匹配的 except。在异常向上传播(导致程序终止)之前finally 块被执行,这是它保证清理操作的核心特性。

设计哲学
#

场景tryexcept 执行?else 执行?finally 执行?
成功顺利完成
异常被捕获中途失败
异常未被捕获中途失败 (在程序崩溃前)
  • else 的用途: 让 try 块尽可能小,只包裹真正可能引发目标异常的代码,而将“成功后才应执行的代码”放入 else,使逻辑更清晰。
  • finally 的用途: 保证资源的释放。无论程序如何退出,它都能确保关闭文件、释放锁等关键清理操作得以执行,防止资源泄漏。
  • 📌 最佳实践: 对于文件等资源,优先使用 with 语句。它在内部自动管理了 try...finally 逻辑,代码更简洁、更安全。

raise 主动抛出异常
#

可以使用 raise 语句来主动抛出一个异常。

抛出内置或自定义异常
#

def check_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative.")

重新抛出异常
#

except 块中,单独使用 raise 语句可以重新抛出刚刚捕获到的异常,让调用栈的上层继续处理它。

try:
    # ...
except FileNotFoundError as e:
    log_error(e) # 记录错误
    raise        # 重新抛出 FileNotFoundError

异常链 (raise ... from ...)
#

为了不丢失原始异常的上下文,可以使用 raise ... from ... 语法将它们链接起来。

def process_data(data):
    try:
        value = int(data)
    except ValueError as e:
        raise RuntimeError("Failed to process data") from e

回溯信息将同时显示 RuntimeError 和导致它的 ValueError,使问题根源一目了然。

自定义异常
#

通过创建自己的异常类,可以让代码的错误语义更加清晰。自定义异常应直接或间接地继承自内置的 Exception 类。

class InputValidationError(Exception):
    """Raised when an input value is invalid."""
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

try:
    user_input = "invalid-email"
    if "@" not in user_input:
        raise InputValidationError(user_input, "Email address is missing '@'")
except InputValidationError as e:
    print(f"Validation Error: Expression '{e.expression}' failed. Reason: {e.message}")

结论
#

  • 不要滥用 except::避免使用宽泛的 except Exception: 或裸 except:始终捕获你期望处理的、最具体的异常
  • 异常不是控制流:异常处理的是意外情况。对于可预期的、正常的程序流程,不要使用异常来控制。
  • 提供清晰的错误信息:无论是通过自定义异常还是内置异常的消息,都应提供足够的信息帮助开发者定位问题。
Python - 这篇文章属于一个选集。
§ 7: 本文