关于Python异常处理,看这一篇就够了

欢迎大家关注我的公众号「HackDev」,会定期分享一些Python相关知识

当我们用Python编程的时候,常常会出现很多错误,大多数是语法错误,当然也有一些语义错误。例如,我就经常忘记在if语句后面加冒号,然后运行的时候就会报错如下所示。

>>> if 5 % 2 == 1
  File "<stdin>", line 1
    if 5 % 2 == 1
                ^
SyntaxError: invalid syntax

如果我们使用Pycharm等IDE来写代码的时候,这种低级的语法错误会被IDE用红色波浪线标注出来,会比较方便我们解决这些错误。

但是,除了这些错误,我们写Python代码的时候也很可能包含一些逻辑上的错误,例如TypeError就是我们经常遇到的一种错误,它表示数据类型不匹配。

>>> "This year is " + 2020 + "."
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

例如上述代码从字面上理解是字符串的拼接,但是由于2020它是int整型,而不是一个字符串,不能应用于字符串的拼接。所以产生了TypeError异常。这种在Python代码执行过程中发生的非语法错误被称为异常。接下来,本文将介绍如何优雅地在Python中处理异常以及抛出异常。

 

异常处理的基本形式

处理异常的标准方法就是使用try...except语句。这一点其实比较类似于Java中的try...catch语句,事实上,大部分语言都有类似的捕捉异常的方法。

通常来说,可能产生异常的代码应该被try语句囊括进去,如果报异常的就会立即停止try语句中的剩余代码,并执行except语句中的代码。

我们可以看一个简单示例

>>> # Declare a function that can handle ZeroDivisionError
>>> def divide_twelve(number):
...     try:
...         print(f"Result: {12/number}")
...     except ZeroDivisionError:
...         print("You can't divide 12 by zero.")
...
>>> # Use the function
>>> divide_twelve(6)
Result: 2.0
>>> divide_twelve(0)
You can't divide 12 by zero.

我们都知道0不能做分母,因此在上述代码中,当0为分母时就产生了一个异常。于是就执行except语句中的代码了。

为什么要处理异常?

为什么要处理异常?因为未经处理的异常会直接中断Python程序的运行,举个例子,假如我们部署一个Python写的网站,其中一个用户的操作触发了某个Bug,导致程序产生了异常,Web服务就会直接被终止,所有用户都不能访问使用了,这显然会造成巨大损失。

>>> # Define a function without handling
>>> def division_no_handle(x):
...     print(f"Result: {20/x}")
...     print("division_no_handle completes running")
...
>>> # Define a function with handling
>>> def division_handle(x):
...     try:
...         print(f"Result: {20/x}")
...     except ZeroDivisionError:
...         print("You can't divide a number with zero.")
...     print("division_handle completes running")
...
>>> # Call the functions
>>> division_handle(0)
You can't divide a number with zero.
division_handle completes running
>>> division_no_handle(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in division_no_handle
ZeroDivisionError: division by zero

通过上面的代码,我们也可以发现,当我们调用division_handle(0)时,由于异常得到了处理,程序还能继续运行,但是,当我们调用没有处理异常的division_no_handle(0)时,程序就直接中断了。

变量分配

我们可以将异常赋值给一个变量,从而进一步了解该异常的相关信息,方便对该异常进行处理。例如在下面代码中,我们可以将处理后的TypeError异常赋值给变量e,并将其打印出来。

>>> # A function that show error's detail
>>> def concat_messages(x, y):
...     try:
...         print(f"{x + y}")
...     except TypeError as e:
...         print(f"Argument Error: {e}")
...
>>> # Call the function
>>> concat_messages("Hello, ", 2020)
Argument Error: can only concatenate str (not "int") to str

当然,我们也可以一次捕捉多个异常,我们将可能的异常包装在一个元组中,如下面的代码片段中所示。当我们调用函数时,我们分别触发ValueError和ZeroDivisionError。可以看到两种异常都被捕获到了

>>> # A function that handles multiple exceptions
>>> def divide_six(number):
...     try:
...         formatted_number = int(number)
...         result = 6/formatted_number
...     except (ValueError, ZeroDivisionError) as e:
...         print(f"Error {type(e)}: {e}")
...
>>> # Use the function
>>> divide_six("six")
Error <class 'ValueError'>: invalid literal for int() with base 10: 'six'
>>> divide_six(0)
Error <class 'ZeroDivisionError'>: division by zero

异常处理语句

1,多异常语句

事实上,我们可以通过多个except子句来处理多个异常,每个子句都处理一些特定的异常。如下所示。

>>> # A function that has multiple except clauses
>>> def divide_six(number):
...     try:
...         formatted_number = int(number)
...         result = 6/formatted_number
...     except ValueError:
...         print("This is a ValueError")
...     except ZeroDivisionError:
...         print("This is a ZeroDivisionError")
...
>>> # Use the function
>>> divide_six("six")
This is a ValueError
>>> divide_six(0)
This is a ZeroDivisionError

2,else语句

我们也可以在try ... except代码块中使用else子句。不过需要注意的是,else子句需要出现在except子句之后。只有当try子句完成而没有引发任何异常时,else子句中的代码才会运行。如下例所示。

>>> # A function that has an else clause
>>> def divide_eight(number):
...     try:
...         result = 8/number
...     except:
...         print("divide_eight has an error")
...     else:
...         print(f"Result: {result}")
...
>>> # Use the function
>>> divide_eight(0)
divide_eight has an error
>>> divide_eight(4)
Result: 2.0

3,finally语句

finally子句放在块的最末尾,将在整个try…except块完成之后运行。只需要记住,无论是否产生异常,finally子句都会被执行就可以了,写法和else语句是一样的。

抛出异常

前面我们学习了使用try ... except块来处理Python中异常,接下来我们来看看抛出异常(产生)异常的基本形式。

>>> # Raise exceptions
>>> raise Exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception
>>> raise NameError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError
>>> raise ValueError()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError

如上所示,Python中使用raise关键字(Java中是throw关键字)后面跟上异常类(例如Exception,NameError)的方式来抛出异常。我们还可以使用异常类构造函数来创建实例,例如ValueError()。这两种用法没有区别,前者只是后者使用构造函数的语法糖。

1,自定义异常信息

我们还可以提供有关我们提出的异常的其他信息。最简单的方法是使用异常类构造函数,幷包含适用的错误消息来创建实例。如下所示:

>>> raise ValueError("You can't divide something with zero.")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: You can't divide something with zero.
>>> raise NameError("It's silly to make this mistake.")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: It's silly to make this mistake.

2,再抛出异常
如图所示,所有异常都会在被捕获时进行处理。但是,有可能我们可以重新抛出异常并将异常传递给外部范围,以查看是否可以处理该异常。

当我们编写涉及嵌套结构的复杂代码时(例如,一个函数调用另一个函数,该函数可能会调用另一个函数),此功能会更加有用。通过再抛出异常,我们可以决定在哪里处理特定异常。当然,根据具体情况确定处理特定异常的确切位置。我们先来看一些代码:

>>> # Define two functions with one calling the other
>>> def cast_number(number_text, to_raise):
...     try:
...         int(number_text)
...     except:
...         print("Failed to cast")
...         if to_raise:
...             print("Re-raise the exception")
...             raise
...
>>> def run_cast_number(number_text, to_raise):
...     try:
...         cast_number(number_text, to_raise)
...     except:
...         print("Handled in run_cast_number")
...
>>> # Use the functions
>>> run_cast_number("six", False)
Failed to cast
>>> run_cast_number("six", True)
Failed to cast
Re-raise the exception
Handled in run_cast_number

在上面的代码中,我们有两个函数,其中run_cast_number调用另一个函数cast_number。我们使用字符串两次调用该函数,这两个函数都会导致异常,因此将显示消息“ Fasted to cast”,因为该异常是在cast_number函数中处理的。但是,第二次调用函数,我们要求cast_number函数重新引发异常,以便except子句在run_cast_number函数中运行。

3,用户自定义异常

在许多情况下,我们可以使用内置的异常来帮助我们在项目中引发和处理异常。但是,Python使我们可以灵活地创建自己的自定义异常类。也就是说,我们需要将一个类声明为内置Exception类的子类。

>>> # Define a custom exception class
>>> class FileExtensionError(Exception):
... def __init__(self, filename, desired_ext):
... self.filename = filename
... self.desired_ext = desired_ext
... def __str__(self):
... return f"File {self.filename} should have the extension: {self.desired_ext}."
...
>>> # Raise custom exceptions
>>> raise FileExtensionError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'filename' and 'desired_ext'
>>> raise FileExtensionError("test.xls", "csv")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.FileExtensionError: File test.xls should have the extension: csv.

如上所示,我们创建了一个名为FileExtensionError的自定义异常类。当我们抛出这个异常时,仅使用类名是行不通的。相反,我们应该通过为构造方法设置两个位置参数来实例化此异常。通过实现__str__方法,我们可以看到自定义异常消息。换句话说,异常消息是通过调用str()函数生成的。

 

4, 何时应该抛出异常

一般来说,当代码可能会在某些情况下会报错无法执行时,就应该抛出异常。

 

最后,再次安利一下我的公众号「HackDev」,会定期分享一些Python相关知识。

参考资料:https://medium.com/better-programming/how-to-handle-and-raise-exceptions-in-python-12-things-to-know-4dfef7f02e4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章