异常
Python 使用称为异常的特殊对象来管理程序执行期间出现的错误。 每当发生使 Python 不确定下一步该做什么的错误时,它都会创建一个异常对象。 如果您编写处理异常的代码,程序将继续运行。 如果您不处理异常,程序将停止并显示回溯,其中包括所引发异常的报告。
使用 try-except 块处理异常。 try-except 块要求 Python 做某事,但它也告诉 Python 在出现异常时该怎么做。 当您使用 try-except 块时,即使出现问题,您的程序也会继续运行。 用户将看到您编写的友好错误消息,而不是用户阅读时可能会感到困惑的回溯。
处理 ZeroDivisionError 异常
让我们看一下导致 Python 引发异常的简单错误。 您可能知道不可能将数字除以零,但我们还是让 Python 来做:
print(5/0)
Python 做不到这一点,所以我们得到一个回溯:
Traceback (most recent call last):
File "division_calculator.py", line 1, in <module>
print(5/0)
~^~
ZeroDivisionError: division by zero (1)
1 | 回溯中报告的错误 ZeroDivisionError 是一个异常对象。 |
Python 创建这种对象是为了应对它不能按照我们的要求去做的情况。 发生这种情况时,Python 会停止程序并告诉我们引发的异常类型。 我们可以使用这些信息来修改我们的程序。 当这种异常发生时,我们将告诉 Python 做什么; 这样,如果它再次发生,我们就会做好准备。
使用 try-except 代码块
当您认为可能会发生错误时,您可以编写一个 tryexcept 块来处理可能引发的异常。 您告诉 Python 尝试运行一些代码,并告诉它如果代码导致特定类型的异常该怎么办。
这是用于处理 ZeroDivisionError 异常的 try-except 块的样子:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
我们将导致错误的行 print(5/0) 放在 try 块中。 如果 try 块中的代码有效,Python 会跳过 except 块。 如果 try 块中的代码导致错误,Python 会查找其错误与引发的错误相匹配的 except 块,并运行该块中的代码。
在这个例子中,try 块中的代码产生了一个 ZeroDivisionError,所以 Python 寻找一个 except 块告诉它如何响应。 然后 Python 运行该块中的代码,用户会看到一条友好的错误消息,而不是回溯:
You can't divide by zero!
如果 try-except 块后面有更多代码,程序将继续运行,因为我们告诉 Python 如何处理错误。 让我们看一个示例,其中捕获错误可以让程序继续运行。
使用异常避免崩溃
当错误发生后程序还有更多工作要做时,正确处理错误尤为重要。 这种情况经常发生在提示用户输入的程序中。 如果程序适当地响应无效输入,它可以提示更多有效输入而不是崩溃。
让我们创建一个只做除法的简单计算器:
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ") (1)
if first_number == 'q':
break
second_number = input("Second number: ") (2)
if second_number == 'q':
break
answer = int(first_number) / int(second_number) (3)
print(answer)
1 | 该程序提示用户输入 first_number , |
2 | 如果用户没有输入 q 退出,则提示 second_number 。 |
3 | 然后我们将这两个数字相除得到答案。 该程序不处理错误,因此要求它除以零会导致它崩溃: |
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division_calculator.py", line 11, in <module>
answer = int(first_number) / int(second_number)
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
ZeroDivisionError: division by zero
程序崩溃很糟糕,但让用户看到回溯也不是一个好主意。 非技术用户会被他们弄糊涂,而在恶意环境中,攻击者会学到比你希望他们知道的更多的东西。 例如,他们会知道您的程序文件的名称,并且会看到您的部分代码无法正常工作。 熟练的攻击者有时可以使用此信息来确定对您的代码使用哪种攻击。
else 代码块
我们可以通过将可能产生错误的行包装在 try-except 块中来使该程序更能抵抗错误。 错误发生在执行除法的行上,因此我们将在此处放置 try-except 块。 此示例还包括一个 else 块。 任何依赖于成功执行的 try 块的代码都进入 else 块:
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
if second_number == 'q':
break
try: (1)
answer = int(first_number) / int(second_number)
except ZeroDivisionError: (2)
print("You can't divide by 0!")
else: (3)
print(answer)
我们要求 Python 在一个 try 块 ❶ 中尝试完成除法运算,其中仅包含可能导致错误的代码。 任何依赖于 try 块的代码都会被添加到 else 块中。 在这种情况下,如果除法操作成功,我们使用 else 块打印结果 ❸。 except 块告诉 Python 在出现 ZeroDivisionError 时如何响应 ❷。 如果 try 块因为被零除错误而没有成功,我们会打印一条友好的消息,告诉用户如何避免这种错误。 程序继续运行,用户永远看不到回溯:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q
唯一应该放在 try 块中的代码是可能导致引发异常的代码。 有时你会有额外的代码,只有在 try 块成功时才应该运行; 此代码进入 else 块。 except 块告诉 Python 在尝试运行 try 块中的代码时出现特定异常时该怎么做。
通过预测可能的错误来源,您可以编写健壮的程序,即使遇到无效数据和资源丢失也能继续运行。 您的代码将能够抵抗无辜的用户错误和恶意攻击。
处理 FileNotFoundError 异常
处理文件时的一个常见问题是处理丢失的文件。 您要查找的文件可能位于不同的位置,文件名可能拼写错误,或者该文件可能根本不存在。 您可以使用 try-except 块处理所有这些情况。
让我们尝试读取一个不存在的文件。 下面的程序试图读取爱丽丝梦游仙境的内容,但我没有将文件 alice.txt 保存在与 alice.py 相同的目录中:
from pathlib import Path
path = Path('alice.txt')
contents = path.read_text(encoding='utf-8')
请注意,我们在这里使用 read_text() 的方式与您之前看到的略有不同。 当系统的默认编码与正在读取的文件的编码不匹配时,需要使用编码参数。 当读取不是在您的系统上创建的文件时,最有可能发生这种情况。
Python 无法读取丢失的文件,因此会引发异常:
Traceback (most recent call last):
File "alice.py", line 4, in <module> (1)
contents = path.read_text(encoding='utf-8') (2)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../pathlib.py", line 1056, in read_text
with self.open(mode='r', encoding=encoding, errors=error
s) as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../pathlib.py", line 1042, in open
return io.open(self, mode, buffering, encoding, errors, n
ewline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt' (3)
这是一个比我们之前看到的更长的回溯,所以让我们看看如何理解更复杂的回溯。 通常最好从回溯的最后开始。 在最后一行,我们可以看到引发了 FileNotFoundError 异常。 这很重要,因为它告诉我们在我们将要编写的 except 块中使用哪种异常。
回过头来看 traceback ❶ 的开头,我们可以看到错误发生在文件 alice.py 的第 4 行。 下一行显示了导致错误的代码行 ❷。 回溯的其余部分显示了一些库中涉及打开和读取文件的代码。 您通常不需要通读或理解回溯中的所有这些行。
为了处理正在引发的错误,try 块将从在回溯中被识别为有问题的行开始。 在我们的示例中,这是包含 read_text() 的行:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError: (1)
print(f"Sorry, the file {path} does not exist.")
1 | 在这个例子中,try 块中的代码产生了一个 FileNotFoundError,因此我们编写了一个匹配该错误的 except 块。 当找不到文件时,Python 然后运行该块中的代码,结果是一条友好的错误消息而不是回溯: |
Sorry, the file alice.txt does not exist.
如果文件不存在,程序将无事可做,所以这就是我们看到的所有输出。 让我们以这个例子为基础,看看当您处理多个文件时异常处理如何提供帮助。
分析文本
您可以分析包含整本书的文本文件。 许多经典文学作品都以简单的文本文件形式提供,因为它们属于公共领域。 本节中使用的文本来自古腾堡计划 (https://gutenber g.org)。 古腾堡计划维护着一系列可在公共领域获得的文学作品,如果您有兴趣在您的编程项目中使用文学文本,这是一个很好的资源。
让我们拉入爱丽丝梦游仙境的文本,并尝试计算文本中的单词数。 为此,我们将使用字符串方法 split(),默认情况下,它会在发现任何空格的地方拆分字符串:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Count the approximate number of words in the file:
words = contents.split() (1)
num_words = len(words) (2)
print(f"The file {path} has about {num_words} words.")
我将文件 alice.txt 移动到正确的目录,所以这次 try 块将起作用。 我们将字符串内容(现在包含爱丽丝梦游仙境的整个文本)作为一个长字符串,并使用 split() 生成书中所有单词的列表❶。 在此列表 ❷ 上使用 len() 可以很好地估计原始文本中的单词数。 最后,我们打印一条语句,报告在文件中找到了多少个单词。 这段代码放在 else 块中,因为它只有在 try 块中的代码执行成功时才有效。
输出告诉我们 alice.txt 中有多少单词:
The file alice.txt has about 29594 words.
计数有点高,因为出版商在此处使用的文本文件中提供了额外的信息,但它是爱丽丝梦游仙境长度的一个很好的近似值。
使用多个文件
让我们添加更多的书来分析,但在我们这样做之前,让我们将这个程序的大部分移动到一个名为 count_words() 的函数。 这将使对多本书运行分析变得更容易:
from pathlib import Path
def count_words(path):
"""Count the approximate number of words in a file.""" (1)
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Count the approximate number of words in the file:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
path = Path('alice.txt')
count_words(path)
此代码的大部分未更改。 它只是缩进了,并移到了 count_words() 的主体中。 在修改程序时保持注释是最新的是一个好习惯,因此注释也被更改为文档字符串并略微改写了❶。
现在我们可以编写一个短循环来计算我们要分析的任何文本中的单词。 为此,我们将要分析的文件的名称存储在列表中,然后为列表中的每个文件调用 count_words()。 我们将尝试计算爱丽丝梦游仙境、悉达多、白鲸记和小妇人的词数,这些词都在公共领域可用。 我特意将 siddhartha.txt 排除在包含 word_count.py 的目录之外,这样我们就可以看到我们的程序如何处理丢失的文件:
from pathlib import Path
def count_words(path):
"""Count the approximate number of words in a file."""
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Count the approximate number of words in the file:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt',
'little_women.txt']
for filename in filenames:
path = Path(filename) (1)
count_words(path)
文件的名称存储为简单的字符串。 然后在调用 count_words() 之前,将每个字符串转换为 Path 对象❶。 丢失的 siddhartha.txt 文件对程序的其余部分没有影响:
The file alice.txt has about 29594 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215864 words.
The file little_women.txt has about 189142 words.
在此示例中使用 try-except 块提供了两个显着优势。 我们阻止我们的用户看到回溯,我们让程序继续分析它能够找到的文本。 如果我们不捕获 siddhartha.txt 引发的 FileNotFoundError,用户将看到完整的回溯,程序将在尝试分析 Siddhartha 后停止运行。 它永远不会分析 Moby Dick 或 Little Women。
静默失败
在前面的示例中,我们通知用户其中一个文件不可用。 但是你不需要报告你捕获的每一个异常。 有时,您会希望程序在发生异常时默默地失败,然后继续运行,就好像什么也没发生一样。 要使程序无声地失败,您可以像往常一样编写一个 try 块,但您明确告诉 Python 在 except 块中什么都不做。 Python 有一个 pass 语句告诉它在一个块中什么都不做:
from pathlib import Path
def count_words(path):
"""Count the approximate number of words in a file."""
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
pass
else:
# Count the approximate number of words in the file:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt',
'little_women.txt']
for filename in filenames:
path = Path(filename)
count_words(path)
此清单与上一个清单之间的唯一区别是 except 块中的 pass 语句。 现在,当引发 FileNotFoundError 时,except 块中的代码会运行,但什么也没有发生。 没有产生回溯,也没有响应引发的错误的输出。 用户会看到每个存在文件的字数统计,但看不到任何文件未找到的迹象:
The file alice.txt has about 29594 words.
The file moby_dick.txt has about 215864 words.
The file little_women.txt has about 189142 words.
pass 语句也充当占位符。 它提醒您,您选择在程序执行的特定点什么也不做,您可能想稍后在那里做一些事情。 例如,在此程序中,我们可能决定将任何丢失的文件名写入名为 missing_files.txt 的文件中。 我们的用户不会看到这个文件,但我们能够读取该文件并处理任何丢失的文本。
决定报告哪些错误
您如何知道何时向您的用户报告错误以及何时让您的程序静默失败? 如果用户知道哪些文本应该被分析,他们可能会喜欢一条消息,告诉他们为什么有些文本没有被分析。 如果用户希望看到一些结果但不知道应该分析哪些书籍,他们可能不需要知道某些文本不可用。 向用户提供他们不需要的信息会降低程序的可用性。 Python 的错误处理结构让您可以细粒度地控制在出现问题时与用户分享多少内容; 由您决定分享多少信息。
编写良好、经过适当测试的代码不太容易出现内部错误,例如语法或逻辑错误。 但是每当你的程序依赖于外部的东西时,比如用户输入、文件的存在或网络连接的可用性,就有可能引发异常。 一点经验将帮助您了解在程序中的何处包含异常处理块,以及向用户报告出现的错误的程度。