异常事件可能是错误(如试图除以零),也可能是通常不会发生的事情。
Python提供功能强大的替代解决方案——异常处理机制。

异常是什么?

Python使用异常对象来表示异常状态,并在遇到错误时引发异常。异常对象未被处理或捕 获)时,程序将终止并显示一条错误消息(traceback)。

1 / 0
'''
ZeroDivisionError  Traceback (most recent call last)
<ipython-input-146-05c9758a9c21> in <module>()
----> 1/0
ZeroDivisionError: division by zero
'''

事实上,每个异常都是某个类(这里是ZeroDivisionError)的实例.

让事情沿你指定的轨道出错

raise 语句

要引发异常,可使用raise语句,并将一个类(必须是Exception的子类)或实例作为参数。将类作为参数时,将自动创建一个实例。

在第一个示例(raise Exception)中,引发的是通用异常,没有指出出现了什么错误。
在第二个示例中,添加了错误消息beyondhuangjiaju。

raise Exception
'''
Exception  Traceback (most recent call last)
<ipython-input-147-fca2ab0ca76b> in <module>()
----> raise Exception

Exception: 
'''
raise Exception('beyondhuangjiaju')
'''
Exception  Traceback (most recent call last)
<ipython-input-149-dfdac40c1137> in <module>()
----> raise Exception('beyondhuangjiaju')

Exception: beyondhuangjiaju
'''

raise ArithmeticError
'''
ArithmeticError  Traceback (most recent call last)
<ipython-input-150-36d9a98b39c2> in <module>()
----> raise ArithmeticError

ArithmeticError: 
'''
raise ArithmeticError("beyondhuangjiaju")
'''
ArithmeticError  Traceback (most recent call last)
<ipython-input-151-a6ed875c1de3> in <module>()
----> 1 raise ArithmeticError("beyondhuangjiaju")

ArithmeticError: beyondhuangjiaju
'''

一些内置的异常类如下表所示:

类名 描述
Exception 几乎所有的异常类都是从它派生而来的
AttributeError 引用属性或给它赋值失败时引发
OSError 操作系统不能执行指定的任务(如打开文件)时引发,有多个子类
IndexError 使用序列中不存在的索引时引发,为LookupError的子类
KeyError 使用映射中不存在的键时引发,为LookupError的子类
NameError 找不到名称(变量)时引发
SyntaxError 代码不正确时引发
TypeError 将内置操作或函数用于类型不正确的对象时引发
ValueError 将内置操作或函数用于这样的对象时引发:其类型正确但包含的值不合适
ZeroDivisionError 在除法或求模运算的第二个参数为零时引发

自定义的异常类

如何创建异常类呢?
就像创建其他类一样,但务必直接或间接地继承Exception(这意味着从任何内置异常类派生都可以)。
当然,如果你愿意,也可在自定义异常类中添加方法。

class SomeCustomBeyondException(Exception): pass

捕获异常

异常比较有趣的地方是可对其进行处理,通常称之为捕获异常
可使用try/except语句。

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except ZeroDivisionError: 
    print("The second number can't be zero!")
    
'''
Enter the first number: 5
Enter the second number: 0
The second number can't be zero!
'''

不用提供参数

来看一个能够“抑制”异常ZeroDivisionError的计算器类。如果启用了这种功能,计算器将打印一条错误消息,而不让异常继续传播。在与用户交互的会话中使用这个计算器时,抑制异常很有用;但在程序内部使用时,引发异常是更佳的选择(此时应关闭“抑制”功能)。

class MuffledCalculator: 
    muffled = False 
    def calc(self, expr): 
        try: 
            return eval(expr) 
        except ZeroDivisionError: 
            if self.muffled: 
                print('Division by zero is illegal') 
            else: 
                raise

calculator = MuffledCalculator()
calculator.calc('10 / 2')#结果为:5.0
calculator.calc('10 / 0')#报错!!!

calculator.muffled = True
calculator.calc('10 / 0')结果为:Division by zero is illegal

如果无法处理异常,在except子句中使用不带参数的raise通常是不错的选择,但有时你可能想引发别的异常。
在这种情况下,导致进入except子句的异常将被作为异常上下文存储起来,并出现在最终的错误消息中。

try:
    1/0
except ZeroDivisionError:
    raise ValueError
#在处理上述异常时,引发了另一个异常:

'''
ZeroDivisionError  Traceback (most recent call last)
<ipython-input-160-14da06562399> in <module>()
      1 try:
----> 2     1/0
      3 except ZeroDivisionError:

ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

ValueError  Traceback (most recent call last)
<ipython-input-160-14da06562399> in <module>()
      2     1/0
      3 except ZeroDivisionError:
----> 4     raise ValueError

ValueError: 
'''

#可使用raise ... from ...语句来提供自己的异常上下文,也可使用None来禁用上下文。
try:
    1/0
except ZeroDivisionError:
    raise ValueError from None
    
'''
ValueError  Traceback (most recent call last)
<ipython-input-161-f4775ad0e53d> in <module>()
      2     1/0
      3 except ZeroDivisionError:
----> 4     raise ValueError from None

ValueError: 
'''

多个 except 子句

当你在输入字符串的时候,此时会引发另一种异常,该程序中的except子句只捕获ZeroDivisionError异常,这种异常将成为漏网之鱼,导致程序终止。

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except ZeroDivisionError: 
    print("The second number can't be zero!")

'''
Enter the first number: 1999
Enter the second number: beyond
---------------------------------------------------------------------------
ValueError  Traceback (most recent call last)
<ipython-input-2-f93dbf72378b> in <module>()
      1 try:
      2     x = int(input('Enter the first number: '))
----> 3     y = int(input('Enter the second number: '))
      4     print(x / y)
      5 except ZeroDivisionError:

ValueError: invalid literal for int() with base 10: 'beyond'
'''

为同时捕获这种异常,可在try/except语句中再添加一个except子句。

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except ZeroDivisionError: 
    print("The second number can't be zero!")
except TypeError:
    print("That wasn't a number,was it?")

一箭双雕

如果要使用一个except子句捕获多种异常,可在一个元组中指定这些异常
在except子句中,异常两边的圆括号很重要。

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except (ZeroDivisionError,TypeError,NameError): 
    print("Your numbers were bogus ...")

在上述代码中,如果用户输入字符串、其他非数字值或输入的第二个数为零,都将打印同样的错误消息。当然,仅仅打印错误消息帮助不大。

捕获对象

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except (ZeroDivisionError,TypeError) as e: 
    print(e)

这里的except子句捕获两种异常,但由于同时显式地捕获了对象本身,因此可将其打印出来,让用户知道发生了什么情况。

一网打尽

即使程序处理了好几种异常,还是可能有一些漏网之鱼。
如果用户在提示时不输入任何内容就按回车键,将出现一条错误消息,这种异常未被try/except语句捕获,这理所当然,因为你没有预测到这种问题,也没有采取相应的措施。

try: 
    x = int(input('Enter the first number: ')) 
    y = int(input('Enter the second number: ')) 
    print(x / y) 
except: 
    print("Something wrong happened...")

'''
Enter the first number: 
Something wrong happened...
'''

像这样捕获所有的异常很危险,因为这不仅会隐藏你有心理准备的错误,还会隐藏你没有考虑过的错误。
在大多数情况下,更好的选择是使用except Exception as e并对异常对象进行检查。
SystemExitKeyboardInterrupt从**BaseException(Exception的超类)**派生而来

万事大吉

使用except Exception as e的小技巧在这个小型除法程序中打印更有用的错误消息。

while True:
    try:
        x = int(input('Enter the first number: ')) 
        y = int(input('Enter the second number: ')) 
        value = x / y
        print("x / y is",value)
    except Exception as e:
        print("Invalid input:",e)
        print('Please try again')
    else:
        break

'''
Enter the first number: 1014
Enter the second number: beyond
Invalid input: invalid literal for int() with base 10: 'beyond'
Please try again
Enter the first number: 1202
Enter the second number: 
Invalid input: invalid literal for int() with base 10: ''
Please try again
Enter the first number: 1014
Enter the second number: 0522
x / y is 1.9425287356321839
'''

最后

最后,还有finally子句,可用于在发生异常时执行清理工作。这个子句是与try子句配套的。
finally子句非常适合用于确保文件或网络套接字等得以关闭。
不管try子句中发生什么异常,都将执行finally子句。

x = None
try:
    x = 1 / 0
except NameError:
    print("Unknown variable")
else:
    print("That went well!")
finally:
    print('Cleaning up ...')
    del x

异常和函数

异常和函数有着天然的联系。
如果不处理函数中引发的异常,它将向上传播到调用函数的地方。
如果在那里也未得到处理,异常将继续传播,直至到达主程序(全局作用域)。
如果主程序中也没有异常处理程序,程序将终止并显示栈跟踪消息。

def faulty():
    raise Exception('Something is wrong')
    
def ignore_exception():
    faulty()
    
def handle_exception():
    try:
        faulty()
    except:
        print('Exception handled')

ignore_exception()
'''
Exception  Traceback (most recent call last)
<ipython-input-6-5ac314d0ac0c> in <module>()
----> 1 ignore_exception()

<ipython-input-5-6806e60d5602> in ignore_exception()
      3 
      4 def ignore_exception():
----> 5     faulty()
      6 
      7 def handle_exception():

<ipython-input-5-6806e60d5602> in faulty()
      1 def faulty():
----> 2     raise Exception('Something is wrong')
      3 
      4 def ignore_exception():
      5     faulty()

Exception: Something is wrong
'''

handle_exception()#结果为:Exception handled

faulty中引发的异常依次从faulty和ignore_exception向外传播,最终导致显示一条栈跟踪消息。
调用handle_exception时,异常最终传播到handle_exception,并被这里的try/except语句处理。

异常之禅

假设有一个字典,你要在指定的键存在时打印与之相关联的值,否则什么都不做。

def describe_person(person):
    print('Description of',person['name'])
    print('Age:',person['age'])
    if 'occupation' in person:
        print('Occupation:',person['occupation'])

上面的这段代码很直观,但效率不高(虽然这里的重点是代码简洁),
因为它必须两次查找occupation’键:
一次检查这个键是否存在(在条件中),
另一次获取这个键关联的值,以便将其打印出来。

def describe_person(person):
    print('Description of',person['name'])
    print('Age:',person['age'])
    try:
        print('Occupation:',person['occupation'])
    except KeyError:
        pass

上面的这个函数直接假设存在’occupation’键。如果这种假设正确,就能省点事:直接获取并打印值,而无需检查这个键是否存在。如果这个键不存在,将引发KeyError异常,而except子句将捕获这个异常。

检查一个对象是否包含属性write

try:
    obj.write
except AttributeError:
    print('The object is not writeable')
else:
    print('The object is writeable')

try子句只是访问属性write,而没有使用它来做任何事情。
如果引发了AttributeError异常,说明对象没有属性write,否则就说明有这个属性。

在很多情况下,相比于使用if/else,使用try/except语句更自然,也更符合Python的风格。

不那么异常的情况

如果你只想发出警告,指出情况偏离了正轨,可使用模块warnings中的函数warn
警告只显示一次。如果再次运行最后一行代码,什么事情都不会发生。

from warnings import warn
warn('I like beyond band')
'''
D:\Anaconda3\lib\site-packages\ipykernel_launcher.py:2: UserWarning: I like beyond band
'''

如果其他代码在使用你的模块,可使用模块warnings中的函数filterwarnings来抑制你发出的警告(或特定类型的警告),并指定要采取的措施,如"error"或"ignore"。

from warnings import filterwarnings
filterwarnings("ignore")
warn("Anyone out there?")
filterwarnings("error")
warn("Something is very wrong!")

'''
UserWarning  Traceback (most recent call last)
<ipython-input-13-2678cd9c1908> in <module>()
      3 warn("Anyone out there?")
      4 filterwarnings("error")
----> 5 warn("Something is very wrong!")

UserWarning: Something is very wrong!
'''

上述引发的异常为UserWarning。发出警告时,可指定将引发的异常(即警告类别),但必须是Warning的子类。如果将警告转换为错误,将使用你指定的异常。另外,还可根据异常来过滤掉特定类型的警告。

from warnings import filterwarnings
filterwarnings("error")
warn("This function is really old...",DeprecationWarning)

'''
DeprecationWarning  Traceback (most recent call last)
<ipython-input-14-db2d386b9ad9> in <module>()
      1 from warnings import filterwarnings
      2 filterwarnings("error")
----> 3 warn("This function is really old...",DeprecationWarning)

DeprecationWarning: This function is really old...
'''

from warnings import filterwarnings
filterwarnings("ignore", category=DeprecationWarning)
warn("Another deprecation warning.", DeprecationWarning)
warn("Something else.")

'''
UserWarning  Traceback (most recent call last)
<ipython-input-15-2ae8758ff90f> in <module>()
      1 filterwarnings("ignore", category=DeprecationWarning)
      2 warn("Another deprecation warning.", DeprecationWarning)
----> 3 warn("Something else.")

UserWarning: Something else.
'''

小结

概念 解释
异常对象 异常情况(如发生错误)是用异常对象表示的。对于异常情况,有多种处理方式;如果忽略,将导致程序终止。
引发异常 可使用raise语句来引发异常。它将一个异常类或异常实例作为参数,但你也可提供两个参数(异常和错误消息)。如果在except子句中调用raise时没有提供任何参数,它将重新引发该子句捕获的异常。
自定义的异常类 你可通过从Exception派生来创建自定义的异常。
捕获异常 要捕获异常,可在try语句中使用except子句。在except子句中,如果没有指定异常类,将捕获所有的异常。你可指定多个异常类,方法是将它们放在元组中。如果向except提供两个参数,第二个参数将关联到异常对象。在同一条try/except语句中,可包含多个except子句,以便对不同的异常采取不同的措施。
else子句 除except子句外,你还可使用else子句,它在主try块没有引发异常时执行。
finally 要确保代码块(如清理代码)无论是否引发异常都将执行,可使用try/finally,并将代码块放在finally子句中。
异常和函数 在函数中引发异常时,异常将传播到调用函数的地方(对方法来说亦如此)。
警告 警告类似于异常,但(通常)只打印一条错误消息。你可指定警告类别,它们是Warning的子类。

本章介绍的新函数

函数 描述
warnings.filterwarnings(action,category=Warning, …) 用于过滤警告
warnings.warn(message, category=None) 用于发出警告