Python3 - 开始python编程(九)
在上一篇文章中,我们深入研究了类,并掌握了它们是如何创建和销毁的。 有了所有这些灵活性,我们还使代码变得复杂。当您拥有复杂的代码库时,一定会遇到错误,这就是错误处理起作用的地方。
首先,错误处理不是测试,也不是打印声明也。那么错误处理到底是什么呢?
错误处理
简而言之,错误处理正在添加足够的逻辑,以便您的程序可以处理本应停止运行的情况。
错误来自可能引发错误的函数。当错误上升时,即使该函数应返回字符串值,也将从函数中将其返回。如果未被捕获,它将导致您的程序崩溃。因此,让我们开始吧,以便我们有效地处理这些问题。
让我们看一些容易发现错误的示例。
1
2
3
4
5
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
print(my_dict['address']) # KeyError: 'address'
my_list: list = [1, 2, 3, 4, 5]
print(my_list[10]) # IndexError: list index out of range
首先,我们试图从”my_dict”访问”address”键。正如预期的那样,该键不存在,并且我们在控制台中收到一个”KeyError”。
其次,我们尝试访问”my_list”中的第 10 个元素(该元素也不存在)。在这种情况下,我们会收到一个”IndexError”。
我们可以使用try ... except
清除它。
1
2
3
4
5
6
7
8
9
10
11
12
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
my_list: list = [1, 2, 3, 4, 5]
try:
print(my_dict['address'])
except:
print('whoops!')
try:
print(my_list[10])
except:
print('whoops again!')
我稍微改变了顺序,但是它的作用与以前相同,只是在较早的时候,我在 try / except 块中包围了失败的代码。
发生的事情是我们通过编写try:
为潜在的代码失败准备了 Python 解释器。在try
主体内部,Python 将在运行每一行时检查错误。如果发生错误,它将退出”try”主体并执行在”except:”内部编写的代码。
我们的程序不再崩溃,但它确实打印出“哇!”然后“再次惊呼!”到控制台。这很好,因为以前我们的程序在遇到第一个失败的”print”语句后立即退出,现在我们的程序继续。如果在最后一个 try / except 语句下编写了更多代码,则该代码也将运行。
我不会到此为止,因为尽管该代码有效,但不是good代码。
让我们以一种正确的方式重写它。
1
2
3
4
5
6
7
8
9
10
11
12
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
my_list: list = [1, 2, 3, 4, 5]
try:
print(my_dict['address'])
except KeyError:
print('whoops!')
try:
print(my_list[10])
except IndexError:
print('whoops again!')
在这里,我们在”except”旁边添加了期望从失败代码中接收到的错误类型。关于所有语言中大多数错误的最酷的事情是,它们往往会告诉您发生了哪个错误,以及导致错误的原因。
我们仍然可以通过捕获错误以显示给用户或登录控制台来对此进行改进。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
my_list: list = [1, 2, 3, 4, 5]
try:
print(my_dict['address'])
except KeyError as error:
print('Bad key: ' + str(error))
try:
print(my_list[10])
except IndexError as error:
print(error)
# Displays the following lines
# "Bad key: 'address'"
# "list index out of range"
现在,我们将获得有关发生的错误的有用信息。使用正确的记录器格式(后文),我们可以包含失败的行号,如果数字 10 存储在变量中,我们甚至可以提供更多信息。
在要求”my_dict”从”my_list”返回值的情况下,我们可以将它们包装在同一”try”块中,如果一个失败,那么我们将停止处理”try”主体的其余部分,执行“除了身体,然后继续。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
my_list: list = [1, 2, 3, 4, 5]
try:
print(my_dict['address'])
print(my_list[10])
except KeyError as error:
print('Bad key: ' + str(error))
except IndexError as error:
print(error)
print("done")
# Displays
# "Bad key: 'address'"
# "done"
由于此操作无法检索字典值,因此它甚至没有尝试检索列表值。它下降到我们最终的”print”语句,程序完成。
对于与网络有关的呼叫,此人可能无法访问 Internet,这可能会很有帮助。由于通常我们有更多依赖于此网络访问的代码,因此我们可以跳过它并节省一些处理时间。这只是一个例子,对网络故障更现实的解决方案是“尽早退出”正在执行网络调用的任何功能,也就是说,只要调用失败,就从该功能中返回。
注意 that 打印错误时,必须将其强制转换为字符串。由于 print 会为您自动在任何类中调用__str __(self)
,因此仅在将错误附加到字符串时才需要这样做。我们也可以使用 f 字符串代替它,这也将为我们做到这一点。
我还应该添加一件事,以确保这是防故障的。那是一个全局例外条款。
所有异常都来自 BaseException 和 Exception 类。两者之间的区别很容易。
BaseException
是所有异常的子类,包括Exception
Exception
是所有非退出异常的子类。根据python 文档:“所有用户定义的异常也应从此类派生。”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my_dict: dict = {'name': 'Bob', 'age': 33, 'language': 'Python'}
my_list: list = [1, 2, 3, 4, 5]
try:
print(my_dict['name'])
print(my_list[10])
except KeyError as error:
print(f'Bad key: {error}')
except IndexError:
print(my_list[-1])
except BaseException as error:
print(f"something went wrong: {error}")
我刚刚添加了另一个 BaseException 异常。这样,如果发生了意外的事情,我们一定会处理并收到一条消息,该消息为我们提供了发生问题的良好提示。
我还对IndexError
例外进行了另一处更改,如果数字超出范围,我们将打印列表中的最后一项。尽管这并非万无一失,但我们更希望列表中的最后一项比第一个总是索引为 0;因此,这是一个足够好的解决方案。可以考虑将此尝试用于“启发式编程”,或更简单地说是最佳猜测逻辑。
这是异常的真正目的,我们不仅要显示它们,我们要对它们采取行动,我们需要对错误进行处理并使之对用户有意义。用户不在乎列表中有多少个元素;他们知道,如果他们想要最后一个,他们将要求从清单中获得很高的价值,并希望它能起作用。
同样地,我们可以将KeyError
异常替换为如下形式:
1
2
3
4
5
6
7
8
9
10
11
12
print(f'Bad key: {error}')
print("Try using one of the following:")
for key in my_dict.keys():
print(f"\t{key}")
# Displays
# Bad key: 'address'
# Try using one of the following:
# name
# age
# language
看到这有多大帮助?使您的错误有意义。
Else
有时,只有在一切顺利的情况下,我们才可能想做某事。我们可以在”try / except”中添加”else”,并且只有在没有任何问题时才会执行。
1
2
3
4
5
6
7
8
9
10
try:
file = open("my_file.txt", 'r')
contents = file.read()
print(contents)
except FileNotFoundError:
print("File not found!")
else:
file.close()
在这里,我们试图打开一个名为”my_file.txt”的只读文件,以”r”表示。如果成功,我们将读取文件的内容并将其存储在”contents”中,然后再将文件的内容打印到控制台。
如果文件与我们正在运行的程序不在同一位置,则将出现FileNotFoundError
。如果发生这种情况,我们会告诉用户发生了什么并继续进行。
如果这没有发生,则我们手头上有一个打开的文件,我们需要确保关闭文件以删除所有锁,以便其他程序以后可以使用该文件。这是else
进入的地方。如果成功,则执行 else 子句,然后关闭文件。
这是有效的代码,因此,如果您要在工作目录中创建一个名为”my_file.txt”的新文件,它将起作用,并且完成后该文件将关闭。
_在 Python 中,文件会通过垃圾回收或程序退出自动关闭。但是,最好关闭所有打开的文件。有一种更好的方法来处理文件 IO,但我们将在以后的文章中讨论。
finally
不,这不是本文的结尾,但是”else”的一个兄弟称为”finally”。两者之间的区别非常容易理解。
else
仅在 try 语句成功时被调用。- 无论如何,最终总是被调用。
它们可以根据需要单独使用或一起使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try:
file = open("my_file.txt", 'r')
print("file opened!")
contents = file.read()
print(contents)
except FileNotFoundError:
print("File not found!")
except Exception as error:
print(f"something went wrong: {error}")
else: # this is optional
file.close()
print("file closed")
finally:
print("done")
如果成功,我们将看到以下输出:
1
2
3
4
file opened!
Hello, World!
file closed
done
如果找不到文件:
1
2
File not found!
done
开始编写”try / except”块的一种极好的方法是将所有内容彼此分开。找出哪些错误可能来自错误引发功能。
照片由Peter Hershey在Unsplash上拍摄
创建自定义错误
有时,我们的程序是如此专业,以至于我们需要自己的错误,因此我们可以从提供特定性的功能返回,以告知用户出了什么问题。我们有两种选择,要么采用 Python 已经存在的错误,要么创建我们自己的错误。
让我们先来看一下从 Python 采纳。
对于此示例,我们将接受一个字符串参数,但我们希望它仅包含空格。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def count_spaces(data: str) -> int:
if '\t' in data:
raise TabError
return data.count(' ')
data = ' 12345 '
try:
print(count_spaces(data))
except TabError:
print("This data has tabs!")
data = '\t12345\t'
try:
print(count_spaces(data))
except TabError:
print("This data has tabs!")
在两个示例中,第一个data
值正确,并且我们在控制台中看到的值为 10。
在第二个示例中,我们使用选项卡(\ t)。我们的功能要做的第一件事是检查数据是否包含选项卡。如果确实如此,则会引发TabError
。错误本身与data
字符串断开连接;它不在乎内容。只是个错误如果事情没有按计划进行,您可以使用它在程序中提供有意义的停止。如果您尝试打印TabError
的字符串值,您将返回None
。
如果您想包括一个更适合当前情况的错误消息,则可以将函数更改为:
1
2
3
4
def count_spaces(data: str) -> int:
if '\t' in data:
raise TabError("You included tabs!")
return data.count(' ')
这将允许您像上面一样使用”TabError as error”,将其显示为错误消息。这是首选方法,因此,您只需要担心返回错误消息,而不必每次调用此函数都创建一条错误消息。
当然,如果错误消息中需要逻辑,则可以调整一条消息或两条消息来满足您的需求。
如果您需要比这更特别的东西,可以随时创建自己的错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BadData(Exception):
"""Raised when the data from a file is not expected"""
pass
try:
file = open("test.txt", "r")
print("File opened")
contents = file.read()
if "level" not in contents:
raise BadData("This file does not contain game levels")
except FileNotFoundError:
print("File not found!")
except BadData as error:
print(error)
else:
file.close()
print("file closed")
在这里,我们创建了一个名为”BadData”的异常,该异常是”Exception”的子类,这是我们应对所有错误使用的。我添加了一个docstring,它可以帮助我们理解在看到”BadData”的自动文档时应该使用的内容。然后我们通过,因为我们不需要做更多的事情。
我们运行我们的程序,如果我们有一个包含关卡的文件,那么我们将继续处理内容。如果不是,那么我们提出我们的例外,并在下面捕获它。因为我们的异常是一个类,所以我们可以在需要它的任何地方使用它。
它简单明了,为您提供了很多功能来处理系统可能无法理解的错误。如果找不到适合您需求的自定义异常,并且没有更好的方法来重构代码以处理该错误,而无需使用”try / except”,请务必创建自定义异常。
重新抛出错误
有时,您可能在函数堆栈深处调用异常,并需要它使气泡冒泡回到顶部。在这种情况下,可以使用raise
来继续引发相同的异常,直到准备好处理它为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def make_error():
raise NotImplementedError
def some_other_func():
try:
make_error()
except NotImplementedError:
raise
try:
some_other_func()
except NotImplementedError:
print("Not implemented")
在这里,我只是使用NotImplementedError
,因为我们还没有完成该功能。 some_other_func 使用 try / except 块调用 make_error。如果收到”NotImplementedError”,我们将再次引发它,以便可以在打印“未实现”的主体中对其进行处理。尽管这是一个简单的示例,但它使您对它的工作原理有了一个很好的了解。
摘要
今天,我们了解了错误是如何发生的,以及如何处理错误。我们学习了几种使程序在不太理想的条件下运行的技术,以及正确的错误处理和糟糕的错误处理之间的区别。如果您下载了 PyCharm,则可以输入”raise”,然后输入”ctrl” +”space”以查看可以使用的一些例外情况。如果您足够勇敢地参与其他地方的模块,那么您甚至可以看到他们为您的项目创建的不同异常。
建议阅读
下一步是什么
接下来是代码结构和可读性。现在该是时候学习如何按照 PEP 8 标准正确构造代码了。我们可能还会介绍 PEP 257 中定义的文档字符串约定。
如果要编写好的 Python 程序,则需要了解这些 PEP 标准并至少遵守它们。
在此之前,请查看是否可以处理某些项目中可能弹出的讨厌错误。做到这一点,以便您可以在应用程序中抛出所需的任何数据,并且可以智能地响应您的疑虑。