Python & AI Tutorials Logo
Python 编程

25. 优雅地处理异常

在第 24 章中,我们学习了当异常发生时如何阅读并理解异常信息。现在我们将学习如何优雅地处理(handle)异常,让程序能够从错误中恢复,而不是直接崩溃。这对于编写健壮、对用户友好的程序至关重要,使其能够应对意外情况。

当 Python 中发生异常时,程序的正常流程会立刻停止。但如果我们能在异常导致程序崩溃之前捕获它呢?如果我们能对错误做出响应,比如让用户再试一次,或者使用默认值,或者记录问题并继续运行呢?这正是异常处理允许我们做到的事情。

25.1) 使用 try 和 except 代码块

25.1.1) try 和 except 的基本结构

try-except 代码块(try-except block) 是 Python 表达“尝试做这件事,如果发生异常,就改做这件事”的方式。基本结构如下:

python
try:
    # 可能会引发异常的代码
    risky_operation()
except:
    # 如果发生任何异常时运行的代码
    print("Something went wrong!")

try 代码块包含可能会引发异常的代码。如果在 try 代码块的任何位置发生异常,Python 会立刻停止执行 try 代码块,并跳转到 except 代码块。如果没有发生异常,则会完全跳过 except 代码块。

我们来看一个具体示例。记得在第 24 章中,尝试把一个无效字符串转换为整数会引发 ValueError

python
# 没有异常处理 - 程序崩溃
user_input = "hello"
number = int(user_input)  # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")

现在我们来优雅地处理这个异常:

python
# 使用异常处理 - 程序继续运行
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except:
    print("That's not a valid number!")
    number = 0  # 使用默认值
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

程序没有崩溃!当 int(user_input) 引发 ValueError 时,Python 跳转到 except 代码块,打印我们的错误消息,设置一个默认值,然后继续执行程序其余部分。

下面是逐步发生的过程:

开始 try 代码块

执行 int 转换

引发异常?

在 try 代码块中继续

跳转到 except 代码块

跳过 except 代码块

执行 except 代码

在 try-except 之后继续

理解“跳转”——实际上发生了什么

当我们说 Python “跳转”到 except 代码块时,我们的意思是它放弃(abandons)正常的顺序执行。这是程序流动方式的根本变化——不仅仅像 if 语句那样的简单分支。让我们通过一个具体例子详细看看:

python
# 通过异常观察执行流程
print("1. Starting program")
 
try:
    print("2. Entered try block")
    number = int("hello")  # 异常在这里发生
    print("3. After conversion")   # 这一行永远不会执行
    result = number * 2            # 这一行永远不会执行
    print("4. After calculation")  # 这一行永远不会执行
except ValueError:
    print("5. In except block - handling the error")
 
print("6. After try-except block")

Output:

1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except block

注意第 3 行和第 4 行从未执行!int("hello") 一引发 ValueError,Python 就会:

  1. 立刻停止执行 try 代码块——就在异常发生的那一行
  2. 查找能够处理该异常类型的匹配 except 子句
  3. 跳转到那个 except 代码块,跳过 try 代码块中剩余的所有代码
  4. 继续在 except 代码块执行完成后,从 try-except 结构之后继续执行

这与正常的程序流程有本质区别。在正常执行中,Python 会按顺序逐行运行。发生异常时,Python 会放弃当前路径并采取一条完全不同的路线。没有异常处理时,程序会在第 2 行崩溃并终止。有了异常处理,程序就能恢复并继续运行。

为什么这很重要:

理解这种“跳转”行为非常关键,因为:

  • try 代码块中异常之后的任何代码都会被跳过——你不能假设 try 代码块后面的行被执行过
  • 变量可能不会被初始化,如果异常发生在赋值之前
  • 你需要规划 except 代码块运行时程序处于什么状态

25.1.2) 安全地处理用户输入

异常处理最常见的用途之一是验证用户输入。用户可以输入任何内容,我们需要优雅地处理无效输入。下面是一个实用示例:程序询问用户年龄:

python
# 使用异常处理安全地输入年龄
print("Please enter your age:")
user_input = input()
 
try:
    age = int(user_input)
    print(f"You are {age} years old.")
    
    # 计算出生年份(假设当前年份是 2024)
    birth_year = 2024 - age
    print(f"You were born around {birth_year}.")
except:
    print("Invalid input! Age must be a number.")
    print("Using default age of 0.")
    age = 0

如果用户输入 "25",输出是:

Please enter your age:
25
You are 25 years old.
You were born around 1999.

如果用户输入 "twenty-five",输出是:

Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.

注意程序是如何优雅地处理错误的,而不是用 traceback 崩溃。这对用户体验要好得多。

25.1.3) 在 try 代码块中处理多个操作

你可以在一个 try 代码块中放入多个操作。如果其中任何一个引发异常,Python 会立刻跳转到 except 代码块。我们先从一个简单示例开始:

python
# try 代码块中的两个操作
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)      # 第一个操作 - 可能引发 ValueError
    result = 100 / number          # 第二个操作 - 可能引发 ZeroDivisionError
    print(f"100 / {number} = {result}")
except:
    print("Something went wrong!")

如果用户输入 "hello",异常发生在第一个操作(转换)处。如果用户输入 "0",异常发生在第二个操作(除法)处。不管哪种情况,我们的单个 except 代码块都会捕获它。

现在我们把它扩展到三个操作:

python
# try 代码块中的多个操作
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)      # 可能引发 ValueError
    denominator = int(denominator_input)  # 可能引发 ValueError
    result = numerator / denominator      # 可能引发 ZeroDivisionError
    print(f"{numerator} / {denominator} = {result}")
except:
    print("Something went wrong with the calculation!")
    print("Make sure you enter valid numbers and don't divide by zero.")

如果用户输入 "10" 和 "2":

Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0

如果用户输入 "10" 和 "zero":

Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

如果用户输入 "10" 和 "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

在这个示例中,有三种不同的情况可能出错:分子转换失败、分母转换失败,或除法失败(如果分母为 0)。我们的单个 except 代码块可以捕获所有这些情况。不过,这种方式有一个限制:我们无法判断具体发生了哪一种错误。我们将在下一节解决这个问题。

25.1.4) 裸 except 子句的问题

使用 except: 而不指定异常类型,被称为裸 except 子句(bare except clause)。虽然它能捕获所有异常,但这通常过于宽泛,会隐藏意料之外的问题。考虑下面的示例:

python
# 裸 except 会捕获一切 - 甚至是我们没预料到的情况
numbers = [10, 20, 30]
 
try:
    index = 5  # 如果 index 越界,我们预期会出现 IndexError
    value = numbers[index]
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

这看起来很合理——我们试图访问一个可能不存在的列表元素。但如果我们的代码里有拼写错误呢?

python
# 如果我们的代码里有拼写错误怎么办?
numbers = [10, 20, 30]
 
try:
    index = 2
    value = numbrs[index]  # Typo: 'numbrs' instead of 'numbers'
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

Output:

Could not access the list element.

except 捕获了由于拼写错误引发的 NameError,并打印 “Could not access the list element.”——这给了我们关于问题原因的错误信息!我们以为是索引越界,但实际上是变量名拼错了。

裸 except 也会捕获 KeyboardInterrupt(用户按 Ctrl+C)以及 SystemExit(程序试图退出),这些通常不应该被捕获。出于这些原因,更好的做法是捕获特定异常,我们接下来就会学习这一点。

25.2) 捕获特定异常

25.2.1) 指定异常类型

与其用裸 except 捕获所有异常,我们可以指定想要处理的异常类型。这让代码更精确,并帮助我们针对不同错误做出恰当响应:

python
# 捕获特定异常类型
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except ValueError:
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

现在我们的 except 子句只会捕获 ValueError 异常。如果发生了不同类型的异常(比如由于拼写错误导致的 NameError),它不会被捕获,我们将看到完整的 traceback——这对调试(debugging)其实很有帮助!

语法是:except ExceptionType:,其中 ExceptionType 是你想捕获的异常类名称(如 ValueErrorTypeErrorZeroDivisionError 等)。

常见错误:捕获了错误的异常类型

如果你指定的异常类型与实际发生的不匹配,会发生什么?我们来看一下:

python
# 捕获错误的异常类型
user_input = "hello"
 
try:
    number = int(user_input)  # This raises ValueError
    print(f"You entered: {number}")
except TypeError:  # But we're catching TypeError!
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'

程序崩溃了!为什么?因为 int("hello") 引发的是 ValueError,而我们的 except 子句只捕获 TypeError。由于没有匹配的 except 子句,异常没有被捕获,程序终止。

这在开发过程中其实很有帮助——如果你捕获了错误的异常类型,你会看到完整 traceback,从而意识到自己的错误。这也是为什么捕获特定异常比使用裸 except 更好的原因之一。

如何避免这个错误:

  1. 阅读 traceback,看看实际发生了哪种异常类型
  2. 在 except 子句中使用那个具体异常类型
  3. 如果你不确定,就运行代码让它崩溃——traceback 会告诉你!

25.2.2) 以不同方式处理不同异常

你可以有多个 except 子句,用不同方式处理不同异常类型。当不同错误需要不同响应时,这会非常有用:

python
# 针对不同异常进行不同处理
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)
    denominator = int(denominator_input)
    result = numerator / denominator
    print(f"{numerator} / {denominator} = {result}")
except ValueError:
    print("Error: Both inputs must be valid integers.")
    print("You entered something that isn't a number.")
except ZeroDivisionError:
    print("Error: Cannot divide by zero.")
    print("The denominator must be a non-zero number.")

如果用户输入 "10" 和 "abc":

Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.

如果用户输入 "10" 和 "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.

Python 会按顺序检查每个 except 子句。当异常发生时,Python 会找到第一个与异常类型匹配的 except 子句并执行其代码块。其他 except 子句会被跳过。

try 代码块中引发异常

匹配第一个 except?

执行第一个 except 代码块

匹配第二个 except?

执行第二个 except 代码块

匹配第三个 except?

执行第三个 except 代码块

异常未捕获 - 向上继续传播

在 try-except 之后继续

25.2.3) 在一个子句中捕获多个异常类型

有时你想用相同方式处理多种不同的异常类型。与其编写多个相同的 except 代码块,你可以在一个子句中捕获多个异常类型:把它们放在括号里作为一个元组(tuple):

python
# 一起捕获多个异常类型
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")
    print("Please enter a non-zero number.")

如果用户输入 "hello":

Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.

如果用户输入 "0":

Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.

ValueError(无效转换)和 ZeroDivisionError(除以零)都由同一个 except 子句处理。当不同错误应该触发相同响应时,这很有用。

25.2.4) 访问异常信息

有时你需要了解关于发生的异常的更多细节。你可以用 as 关键字捕获异常对象(exception object)。但首先,让我们理解异常对象到底是什么。

什么是异常对象?

当 Python 引发异常时,它不只是发出“出了问题”的信号——它会创建一个包含错误信息的对象(object)。这个异常对象就像一份详细的错误报告,包含:

  • 错误消息:对问题的描述
  • 异常类型:发生了哪一类错误(ValueError、TypeError 等)
  • 额外属性:根据异常类型不同而有所不同的特定信息

把异常对象想象成一个容器,里面装着关于错误的所有信息。就像列表对象包含元素并拥有 append() 之类的方法一样,异常对象也包含错误信息,并拥有你可以访问的属性。

当你写 except ValueError as error: 时,你是在告诉 Python:“如果发生 ValueError,创建一个名为 error 的变量,并把异常对象放进去,以便我检查它。”

让我们探索异常对象里有什么:

python
# 检查异常对象的内容
try:
    number = int("hello")
except ValueError as error:
    print("Exception caught! Let's examine it:")
    print(f"Type: {type(error)}")
    print(f"String representation: {error}")
    print(f"Args tuple: {error.args}")

Output:

Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)

异常对象具有:

  • 一个类型(ValueError 类)——这告诉你发生了哪种错误
  • 一个字符串表示形式(错误消息)——这是你在 traceback 中看到的内容
  • 一个 args 属性(包含消息和其他参数的元组)——它提供对错误细节的结构化访问

为什么这很重要:

不同异常类型有不同属性,能提供特定信息。理解异常对象的结构,能帮助你提取对调试或用户反馈有用的信息:

python
# 不同异常具有不同属性
numbers = [10, 20, 30]
 
try:
    value = numbers[10]
except IndexError as error:
    print(f"IndexError message: {error}")
    print(f"Exception args: {error.args}")
 
# 现在换成字典试试
grades = {"Alice": 95}
 
try:
    grade = grades["Bob"]
except KeyError as error:
    print(f"KeyError message: {error}")
    print(f"Missing key: {error.args[0]}")

Output:

IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: Bob

注意 KeyError 的消息里包含了实际缺失的键。不同异常类型会提供不同且有用的信息,你可以通过异常对象来访问它们。

25.3) 在 try 代码块中使用 else 和 finally

25.3.1) else 子句:只在成功时运行的代码

try-except 代码块中的 else 子句只会在 try 代码块中没有发生异常时运行。这对于那些只应该在有风险操作成功时才执行的代码非常有用:

python
# 使用 else 放置仅在成功时运行的代码
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
except ValueError:
    print("That's not a valid number!")
else:
    # 仅当 int(user_input) 成功时才会运行
    print(f"Successfully converted: {number}")
    squared = number ** 2
    print(f"The square of {number} is {squared}")

如果用户输入 "5":

Enter a number:
5
Successfully converted: 5
The square of 5 is 25

如果用户输入 "hello":

Enter a number:
hello
That's not a valid number!

为什么要用 else,而不是把代码直接放到 try 代码块末尾?有两个重要原因:

  1. 清晰性else 子句明确表示这段代码只会在成功时运行
  2. 异常范围:在 else 子句中引发的异常不会被前面的 except 子句捕获

下面是一个示例,展示第二点为什么重要:

python
# 演示为什么 else 对异常范围有用
try:
    number_1 = int(input("Enter a number_1: "))
except ValueError:
    print("Invalid input!")
else:
    # 如果这里发生错误,不会被上面的 except 捕获
    # 这有助于区分输入错误与处理错误
    number_2 = int(input("Enter a number_2: ")) # Could raise ValueError

如果我们把 number_2 = int(input(...))number_1 一起放在 try 代码块中,那么无论哪个输入引发 ValueError,都会被同一个 except ValueError 子句捕获。这样就无法判断是哪个输入导致了问题。

通过把 number_2 = int(input(...)) 放到 else 代码块中,我们把错误处理分离开来。except 子句只捕获来自 number_1 的错误,而来自 number_2 的错误会引发未捕获异常并显示完整 traceback——这会清楚表明是第二次输入失败,而不是第一次。

25.3.2) finally 子句:总会运行的代码

finally 子句包含的代码无论如何都会运行——无论是否发生异常,无论异常是否被捕获。这对必须执行的清理操作至关重要:

python
# 使用 finally 进行清理
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"Result: {result}")
except ValueError:
    print("Invalid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
finally:
    print("Calculation attempt completed.")

如果用户输入 "5":

Enter a number:
5
Result: 20.0
Calculation attempt completed.

如果用户输入 "hello":

Enter a number:
hello
Invalid number!
Calculation attempt completed.

如果用户输入 "0":

Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.

finally 代码块在三种情况下都会运行!这就是 finally 的关键行为:它总是会执行,不管 try 代码块里发生了什么。

开始 try 代码块

引发异常?

完成 try 代码块

异常被捕获?

如果存在则执行 else 代码块

执行匹配的 except 代码块

异常继续传播

执行 finally 代码块

异常被捕获了吗?

在 try-except-finally 之后继续

异常继续向上传播

25.3.3) 组合使用 try、except、else 和 finally

你可以把四个子句全部一起使用,从而创建全面的异常处理:

python
# 完整的异常处理结构
print("Enter a number to calculate its reciprocal:")
user_input = input()
 
try:
    # 有风险的操作
    number = int(user_input)
    reciprocal = 1 / number
except ValueError:
    # 处理转换错误
    print("Error: Input must be a valid integer.")
except ZeroDivisionError:
    # 处理除以零
    print("Error: Cannot calculate reciprocal of zero.")
else:
    # 仅在成功时运行的代码
    print(f"The reciprocal of {number} is {reciprocal}")
    print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
    # 总会运行的清理代码
    print("Reciprocal calculation completed.")

如果用户输入 "4":

Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.

如果用户输入 "hello":

Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.

如果用户输入 "0":

Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.

执行流程是:

  1. try 代码块总是最先执行
  2. 如果发生异常,执行匹配的 except 代码块
  3. 如果没有发生异常,执行 else 代码块(如果存在)
  4. finally 代码块总是最后执行,不管发生了什么

25.4) 使用 raise 主动引发异常

25.4.1) 为什么要引发异常?

到目前为止,我们一直在捕获 Python 自动引发的异常。但有时候你需要在自己的代码里主动(deliberately)引发异常。这在以下情况很有用:

  1. 你检测到一个无效情况,而你的代码无法处理它
  2. 你想强制执行规则或约束
  3. 你想向调用你函数的代码发出错误信号

引发异常是 Python 表达“我无法继续——出了问题,调用我的人需要处理”的方式。

语法很简单:raise ExceptionType("error message")

下面是一个基本示例:

python
# 主动引发异常
age = -5
 
if age < 0:
    raise ValueError("Age cannot be negative!")
 
print(f"Age: {age}")  # 这一行永远不会执行

Output:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!

当 Python 遇到 raise 时,它会立刻创建一个异常并开始寻找可以处理它的 except 代码块。如果没有,程序会以 traceback 终止。

25.4.2) 在函数中引发异常

在函数(function)中引发异常对于验证输入并强制执行约束尤其有用:

python
# 通过引发异常验证输入的函数
def calculate_discount(price, discount_percent):
    """Calculate discounted price.
    
    Args:
        price: Original price (must be positive)
        discount_percent: Discount percentage (must be 0-100)
    
    Returns:
        Discounted price
    
    Raises:
        ValueError: If inputs are invalid
    """
    if price < 0:
        raise ValueError("Price cannot be negative!")
    
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100!")
    
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# 使用该函数
try:
    final_price = calculate_discount(100, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Final price: $80.0

现在我们用无效输入试试:

python
# 无效价格
try:
    final_price = calculate_discount(-50, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Price cannot be negative!
python
# 无效折扣
try:
    final_price = calculate_discount(100, 150)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Discount must be between 0 and 100!

通过引发异常,函数清楚地表达了哪里出了问题。调用方代码随后可以决定如何处理错误——也许让用户输入新值、使用默认值,或记录错误。

25.4.3) 选择正确的异常类型

Python 有很多内置异常类型,选择合适的异常类型会让代码更清晰。下面是验证场景中最常用的异常:

  • ValueError:当值的类型正确但值本身不合适时使用(例如:负年龄、无效百分比)
  • TypeError:当值的类型完全错误时使用(例如:用字符串代替数字)
  • KeyError:当字典键不存在时使用
  • IndexError:当序列索引越界时使用

下面是一个展示不同异常类型的示例:

python
# 使用合适的异常类型
def get_student_grade(grades, student_name):
    """Get a student's grade from the grades dictionary.
    
    Args:
        grades: Dictionary mapping student names to grades
        student_name: Name of the student
    
    Returns:
        The student's grade
    
    Raises:
        TypeError: If grades is not a dictionary
        KeyError: If student_name is not in grades
        ValueError: If the grade is invalid
    """
    if not isinstance(grades, dict):
        raise TypeError("Grades must be a dictionary!")
    
    if student_name not in grades:
        raise KeyError(f"Student '{student_name}' not found!")
    
    grade = grades[student_name]
    
    if not (0 <= grade <= 100):
        raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
    
    return grade
 
# 使用有效数据测试
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
 
try:
    grade = get_student_grade(grades, "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Alice's grade: 95
python
# 测试缺失学生
try:
    grade = get_student_grade(grades, "David")
    print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Student 'David' not found!
python
# 测试错误类型
try:
    grade = get_student_grade("not a dict", "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Grades must be a dictionary!

使用合适的异常类型能帮助其他程序员(以及未来的你)理解发生了哪一类错误。

25.4.4) 重新引发异常

有时你想捕获一个异常,做一些事情(例如记录日志),然后让异常继续向上传播。你可以在 except 代码块内使用不带参数的 raise 来做到这一点:

python
# 记录日志后重新引发异常
def divide_numbers(a, b):
    """Divide two numbers with error logging."""
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("ERROR LOG: Division by zero attempted")
        print(f"  Numerator: {a}, Denominator: {b}")
        raise  # 重新引发同一个异常
 
# 使用该函数
try:
    result = divide_numbers(10, 0)
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Cannot divide by zero!")

Output:

ERROR LOG: Division by zero attempted
  Numerator: 10, Denominator: 0
Cannot divide by zero!

不带参数的 raise 会重新引发刚刚捕获的异常。这在你想要以下行为时很有用:

  1. 记录或保存错误信息
  2. 做一些清理工作
  3. 让错误传播给调用方

25.4.5) 从异常中引发异常

有时你希望在处理一个异常时引发一个新异常,并保留原始错误的上下文。Python 3 提供了 raise ... from ... 语法来实现:

python
# 基于已有异常引发新异常
def load_config(config_dict, key):
    """Load configuration value from dictionary."""
    try:
        config_value = config_dict[key]
        
        # 尝试解析为整数
        parsed_value = int(config_value)
        return parsed_value
        
    except KeyError as error:
        raise RuntimeError(f"Configuration key missing: {key}") from error
    except ValueError as error:
        raise RuntimeError(f"Invalid configuration format for {key}") from error
 
# 使用该函数
config = {"timeout": "30", "retries": "5"}
 
try:
    value = load_config(config, "timeout")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Config value: 30

如果键不存在:

python
try:
    value = load_config(config, "missing_key")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'

from 关键字把新异常与原始异常关联起来。这会创建一条异常链,有助于调试——你既能看到高层次出了什么问题(配置错误),也能看到底层原因是什么(找不到键)。


异常处理是编写可靠程序最重要的工具之一。通过使用 try-except 代码块,你可以预见问题、优雅地处理它们,并为用户提供更好的体验。请记住:

  • 使用 try-except 优雅地处理预期错误
  • 捕获特定异常类型,而不是使用裸 except
  • 使用 else 放置只应在成功时运行的代码
  • 使用 finally 放置必须始终运行的清理代码
  • 在自己的代码中引发异常以表明问题
  • 选择合适的异常类型,让错误更清晰
  • 提供有用的错误消息,解释出了什么问题

在下一章中,我们将学习防御式编程(defensive programming)技术,它将异常处理与输入验证及其他策略结合起来,让程序更加健壮。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai