关于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, 何时应该抛出异常

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,401评论 6 531
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,011评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,263评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,543评论 1 307
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,323评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,874评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,968评论 3 439
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,095评论 0 286
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,605评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,551评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,720评论 1 369
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,242评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,961评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,358评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,612评论 1 280
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,330评论 3 390
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,690评论 2 370

推荐阅读更多精彩内容