Python 中的异常处理入门

160 阅读9分钟

在日常工作中,我总是不失时机的会向我的同事展示我卓越的 Python 编程能力。然而,每当一切都很顺利的完成我的程序,并满怀期待地向同事展示成果时,程序却时常因为 Python 的随机错误而意外崩溃,让原本想展示编程能力的我瞬间变成了尴尬时刻。

这件小事告诉我们,在编写程序时处理错误是何等的重要。

因此,让我们花一点时间来学习如何在 Python 中进行异常处理是很有必要的。在本篇教程中,我们将专注于学习异常处理的基础概念和技巧。

在开始学习异常处理之前,你应当对 Python 的基础知识有扎实的掌握。了解异常抛出的原因是有效处理异常的前提!

Python 中的 Try 和 Except 语句

tryexcept 语句是构成异常处理的主要方法。它们的结构看起来如下:

x = 0
try:
    print(5 / x)
except ZeroDivisionError:
    print("Something went wrong")

# Something went wrong

让我们逐行解释一下上面的代码:

  1. 第 1 行为变量 x 赋值 0
  2. 第 2 行开始了一个 try 子句
  3. 第 3 行尝试执行一个除法运算并打印其值,即5除以 x 的值。但由于 x 的值是 0,数学上不能除以 0,这会引发一个 ZeroDivisionError 异常
  4. 第 4 行定义了一个 except 子句,专门用来捕获和处理 ZeroDivisionError 类型的异常。当 try 块中的代码引发了 ZeroDivisionError 时,程序执行会跳转到这个 except 子句
  5. 第 5 行打印错误信息

以上代码,由于 x 被设置为 0,执行 5 / x 时会发生 ZeroDivisionError 异常,因此此程序最终会输出 "Something went wrong" 而不是除法的执行结果。

如果我们删除 tryexcept 子句,程序将会在尝试执行 5 / x 时立即终止,这是由于 Python 在没有明确定义的情况下不知道如何处理异常。

综上,处理异常的整个思路:当程序遇到无法直接忽略的错误时,我们需要告诉它该怎么处理。接下来,让我们进一步探讨 tryexcept 子句的具体运作方式。

分解 try 语句

TryExcept 语句遵循一种特定的模式,该模式可以确保我们能够可靠地处理代码中的所有问题。现在,让我们梳理一下这种模式。

程序执行的流程的第一步是尝试执行 try 子句内的代码。

随后,程序有三种可能的执行情况:

  1. Try 子句中没有发生错误

如果 try 子句内的代码执行时没有出现任何错误,则程序执行流程如下:

  1. 执行 try 子句
  2. 跳过所有 except 子句
  3. 继续正常运行
x = 1
try:
    print(5 / x)
except ZeroDivisionError:
    print("Something went wrong")

print("I am executing after the try clause!")

# 5.0
# I am executing after the try clause!

在这个经过修改的示例中,try 子句内的代码(第 3 行)没有出现任何问题。程序将跳过 except 子句,继续执行 tryexcept 语句之后的代码。

  1. Try 子句中发生了错误并定义了相关异常类型处理方法

如果 try 子句中的代码引发了异常,并且使用 except 关键字定义了与之匹配的异常类型处理方法,程序将按如下流程执行:

  1. 跳过 try 子句中的剩余代码
  2. 执行与之匹配异常类型的 except 子句内的代码
  3. 继续正常运行
x = 0
try:
    print(5 / x)
except ZeroDivisionError:
    print("Something went wrong")
    
print("I am executing after the try clause!")

# Something went wrong
# I am executing after the try clause!

以上代码,我们将变量 x 重新改为 0,并尝试执行 5 除以 x 操作,这将导致一个 ZeroDivisionError 异常。由于我们的 except 语句指定了这种类型的异常,因此该 except 子句内的代码会在程序继续正常运行之前执行。

  1. Try 子句中发生了错误但并未定义相关异常类型处理方式

如果程序在 try 子句中抛出异常,但所有 except 子句均未明确捕获该异常类型,则程序将终止执行并显示未处理的异常信息。

x = 0
try:
    print(5 / y)
except ZeroDivisionError:
    print("Something went wrong")

print("I am executing after the try clause!")

# NameError: name 'y' is not defined

在上面的示例中,我们试图用 5 除以未定义的变量 y,这会引发 NameError 错误。由于我们未定义 NameError 错误的处理方法,程序将不得终止执行。

Finally 语句

在 Python 中,finally 关键字用于定义在 try 语句执行完毕后总是会执行的代码块,无论是否发生了异常。finally 子句通常用于执行清理工作,如关闭文件、释放资源、解锁等。

x = 0
try:
    print(5 / x)
except ZeroDivisionError:
    print("I am the except clause!")
finally:
    print("I am the finally clause!")

print("I am executing after the try clause!")

# I am the except clause!
# I am the finally clause!
# I am executing after the try clause!

在这个示例中,我们继续演示 ZeroDivisionError 错误。我们将看到如下执行流程:

  1. try 子句内抛出异常
  2. 执行 except 子句内的代码
  3. 执行 finally 子句内的代码
  4. 继续正常运行

如果我们修改代码,使 try 子句不再抛出异常,我们仍然会看到类似的执行顺序,只是 try 子句正常执行代替了 except 子句执行。

x = 1
try:
    print(5 / x)
except ZeroDivisionError:
    print("I am the except clause!")
finally:
    print("I am the finally clause!")

print("I am executing after the try clause!")

# 5.0
# I am the finally clause!
# I am executing after the try clause!

上面的代码,try 子句成功执行,未抛出任何异常。finally 子句和程序后续代码都将按照我们的预期执行。

在某些情况下,无论 try 子句是否抛出异常都必须执行清理操作的情况下,finally 子句会非常有用。例如,关闭数据库连接、关闭文件句柄或进行日志记录等操作,都非常适合使用 finally 子句。

Else 语句

此外,还有另一个可选的子句,即 else 子句。其工作原理很简单:如果 try 子句中的代码在执行过程中没有引发任何错误,那么 else 子句中的代码将被执行。

x = 1
try:
    print(5 / x)
except ZeroDivisionError:
    print("I am the except clause!")
else:
    print("I am the else clause!")
finally:
    print("I am the finally clause!")

print("I am executing after the try clause!")

# 5.0
# I am the else clause!
# I am the finally clause!
# I am executing after the try clause!

上面的代码执行流程如下:

  1. 执行 try 子句
  2. 执行 else 子句
  3. 执行 finally 子句
  4. 继续正常运行

如果我们在 try 子句中引发任何异常,else 子句将被忽略。

x = 0
try:
    print(5 / x)
except ZeroDivisionError:
    print("I am the except clause!")
else:
    print("I am the else clause!")
finally:
    print("I am the finally clause!")

print("I am executing after the try clause!")

# I am the except clause!
# I am the finally clause!
# I am executing after the try clause!

内置异常

目前为止,我们已经接触了两种不同的异常类型:NameErrorZeroDivisionError。如果需要处理其它类型的异常,我们该如何做呢?

Python 标准库提供了一系列异常类型,几乎涵盖了各类异常处理的全部需求,满足开发者应对各种编程场景。

下面是一些常见的异常类型:

  • KeyError - 尝试访问字典中不存在的键时触发
  • IndexError - 尝试访问一个超出序列范围的索引时触发
  • TypeError - 尝试对一个对象执行一个它不支持的操作时触发
  • OSError - 尝试执行一个操作系统无法完成的操作时触发

Python 官方文档提供了更多异常类型相关内容,推荐大家去看看,这样也能帮助我们了解 Python 程序可能遇到的各种错误。

自定义异常

在 Python 中,我们也可以通过创建一个新的类来定义自己的异常,这个类通常应该继承自内置的 Exception 类或者它的某个子类。

class ForError(Exception):
    def __init__(self, message):
        self.message = message
    
    def foo(self):
        print("bar")

在上面的示例中,我们创建了一个新类,它从 Exception 类继承而来。现在,我们能够为这个异常类添加自定义功能,并且可以像操作普通对象一样操作这个异常类。

try:
    raise FooError("This is a test error")
except FooError as e:
    e.foo()

# bar

以上代码,我们故意触发了自定义的 FooError 异常,使用 except 子句捕获了该异常类型,并将其别名为 e。现在,我们可以通过别名 e 访问自定义异常中的 foo() 方法了。

自定义异常为错误处理提供了更大的灵活性。我们可以通过自定义异常实现更详尽的日志记录、更精准的错误追踪机制,以及任何根据需求定制的功能。

性能方面的考虑

现在,我们已经了解了 tryexcept 和异常类型的基础知识,可以在代码中使用它们优雅地处理错误了。但是,这是否会对代码性能产生重大影响呢?

简短的回答是否定的。Python 3.11 之后发布的版本中,在没有抛出异常的情况下,使用 tryexcept 语句几乎不会降低运行速度。

捕获错误确实会使程序运行速度有所下降,然而,相较于程序的崩溃,及时捕获并处理这些错误显然是更好的选择。

在 Python 早期版本中,使用 tryexcept 语句会导致一些额外的运行时开销。如果你使用的不是最新版本,请留意这一点。

结语

在本教程中,我们学习了 tryexceptelsefinally 子句的使用及其执行顺序,以及在什么情况下执行哪些子句,我们还学习了创建自定义异常的基础知识。

此外,最重要的是要记住,只要有风险和容易出错的代码,我们都应该尝试捕获错误并处理错误,这会使我们的代码更具健壮性,也会让我们显得更加专业和技术高超。