25. 优雅地处理异常
在第 24 章中,我们学习了当异常发生时如何阅读并理解异常信息。现在我们将学习如何优雅地处理(handle)异常,让程序能够从错误中恢复,而不是直接崩溃。这对于编写健壮、对用户友好的程序至关重要,使其能够应对意外情况。
当 Python 中发生异常时,程序的正常流程会立刻停止。但如果我们能在异常导致程序崩溃之前捕获它呢?如果我们能对错误做出响应,比如让用户再试一次,或者使用默认值,或者记录问题并继续运行呢?这正是异常处理允许我们做到的事情。
25.1) 使用 try 和 except 代码块
25.1.1) try 和 except 的基本结构
try-except 代码块(try-except block) 是 Python 表达“尝试做这件事,如果发生异常,就改做这件事”的方式。基本结构如下:
try:
# 可能会引发异常的代码
risky_operation()
except:
# 如果发生任何异常时运行的代码
print("Something went wrong!")try 代码块包含可能会引发异常的代码。如果在 try 代码块的任何位置发生异常,Python 会立刻停止执行 try 代码块,并跳转到 except 代码块。如果没有发生异常,则会完全跳过 except 代码块。
我们来看一个具体示例。记得在第 24 章中,尝试把一个无效字符串转换为整数会引发 ValueError:
# 没有异常处理 - 程序崩溃
user_input = "hello"
number = int(user_input) # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")现在我们来优雅地处理这个异常:
# 使用异常处理 - 程序继续运行
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 代码块,打印我们的错误消息,设置一个默认值,然后继续执行程序其余部分。
下面是逐步发生的过程:
理解“跳转”——实际上发生了什么
当我们说 Python “跳转”到 except 代码块时,我们的意思是它放弃(abandons)正常的顺序执行。这是程序流动方式的根本变化——不仅仅像 if 语句那样的简单分支。让我们通过一个具体例子详细看看:
# 通过异常观察执行流程
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 就会:
- 立刻停止执行 try 代码块——就在异常发生的那一行
- 查找能够处理该异常类型的匹配 except 子句
- 跳转到那个 except 代码块,跳过 try 代码块中剩余的所有代码
- 继续在 except 代码块执行完成后,从 try-except 结构之后继续执行
这与正常的程序流程有本质区别。在正常执行中,Python 会按顺序逐行运行。发生异常时,Python 会放弃当前路径并采取一条完全不同的路线。没有异常处理时,程序会在第 2 行崩溃并终止。有了异常处理,程序就能恢复并继续运行。
为什么这很重要:
理解这种“跳转”行为非常关键,因为:
- try 代码块中异常之后的任何代码都会被跳过——你不能假设 try 代码块后面的行被执行过
- 变量可能不会被初始化,如果异常发生在赋值之前
- 你需要规划 except 代码块运行时程序处于什么状态
25.1.2) 安全地处理用户输入
异常处理最常见的用途之一是验证用户输入。用户可以输入任何内容,我们需要优雅地处理无效输入。下面是一个实用示例:程序询问用户年龄:
# 使用异常处理安全地输入年龄
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 代码块。我们先从一个简单示例开始:
# 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 代码块都会捕获它。
现在我们把它扩展到三个操作:
# 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)。虽然它能捕获所有异常,但这通常过于宽泛,会隐藏意料之外的问题。考虑下面的示例:
# 裸 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.")这看起来很合理——我们试图访问一个可能不存在的列表元素。但如果我们的代码里有拼写错误呢?
# 如果我们的代码里有拼写错误怎么办?
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 捕获所有异常,我们可以指定想要处理的异常类型。这让代码更精确,并帮助我们针对不同错误做出恰当响应:
# 捕获特定异常类型
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 是你想捕获的异常类名称(如 ValueError、TypeError、ZeroDivisionError 等)。
常见错误:捕获了错误的异常类型
如果你指定的异常类型与实际发生的不匹配,会发生什么?我们来看一下:
# 捕获错误的异常类型
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 更好的原因之一。
如何避免这个错误:
- 阅读 traceback,看看实际发生了哪种异常类型
- 在 except 子句中使用那个具体异常类型
- 如果你不确定,就运行代码让它崩溃——traceback 会告诉你!
25.2.2) 以不同方式处理不同异常
你可以有多个 except 子句,用不同方式处理不同异常类型。当不同错误需要不同响应时,这会非常有用:
# 针对不同异常进行不同处理
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 子句会被跳过。
25.2.3) 在一个子句中捕获多个异常类型
有时你想用相同方式处理多种不同的异常类型。与其编写多个相同的 except 代码块,你可以在一个子句中捕获多个异常类型:把它们放在括号里作为一个元组(tuple):
# 一起捕获多个异常类型
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 的变量,并把异常对象放进去,以便我检查它。”
让我们探索异常对象里有什么:
# 检查异常对象的内容
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 属性(包含消息和其他参数的元组)——它提供对错误细节的结构化访问
为什么这很重要:
不同异常类型有不同属性,能提供特定信息。理解异常对象的结构,能帮助你提取对调试或用户反馈有用的信息:
# 不同异常具有不同属性
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 代码块中没有发生异常时运行。这对于那些只应该在有风险操作成功时才执行的代码非常有用:
# 使用 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 代码块末尾?有两个重要原因:
- 清晰性:
else子句明确表示这段代码只会在成功时运行 - 异常范围:在
else子句中引发的异常不会被前面的except子句捕获
下面是一个示例,展示第二点为什么重要:
# 演示为什么 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 子句包含的代码无论如何都会运行——无论是否发生异常,无论异常是否被捕获。这对必须执行的清理操作至关重要:
# 使用 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 代码块里发生了什么。
25.3.3) 组合使用 try、except、else 和 finally
你可以把四个子句全部一起使用,从而创建全面的异常处理:
# 完整的异常处理结构
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.执行流程是:
try代码块总是最先执行- 如果发生异常,执行匹配的
except代码块 - 如果没有发生异常,执行
else代码块(如果存在) finally代码块总是最后执行,不管发生了什么
25.4) 使用 raise 主动引发异常
25.4.1) 为什么要引发异常?
到目前为止,我们一直在捕获 Python 自动引发的异常。但有时候你需要在自己的代码里主动(deliberately)引发异常。这在以下情况很有用:
- 你检测到一个无效情况,而你的代码无法处理它
- 你想强制执行规则或约束
- 你想向调用你函数的代码发出错误信号
引发异常是 Python 表达“我无法继续——出了问题,调用我的人需要处理”的方式。
语法很简单:raise ExceptionType("error message")
下面是一个基本示例:
# 主动引发异常
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)中引发异常对于验证输入并强制执行约束尤其有用:
# 通过引发异常验证输入的函数
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现在我们用无效输入试试:
# 无效价格
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!# 无效折扣
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:当序列索引越界时使用
下面是一个展示不同异常类型的示例:
# 使用合适的异常类型
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# 测试缺失学生
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!# 测试错误类型
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 来做到这一点:
# 记录日志后重新引发异常
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 会重新引发刚刚捕获的异常。这在你想要以下行为时很有用:
- 记录或保存错误信息
- 做一些清理工作
- 让错误传播给调用方
25.4.5) 从异常中引发异常
有时你希望在处理一个异常时引发一个新异常,并保留原始错误的上下文。Python 3 提供了 raise ... from ... 语法来实现:
# 基于已有异常引发新异常
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如果键不存在:
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)技术,它将异常处理与输入验证及其他策略结合起来,让程序更加健壮。