Python 最强大的结构之一就是它的异常处理能力,所有的标准异常都使用类来实现,都是基类 Exception 的成员,都从基类 Exception 继承,而且都在 exceptions 模块中定义。Python 自动将所有异常名称放在内建命名空间中,所以程序不必导入 exceptions 模块即可使用异常。一旦引发而且没有捕捉 SystemExit 异常,程序执行就会终止。异常的处理过程、如何引发或抛出异常及如何构建自己的异常类都是需要深入理解的。

什么是异常

错误

从软件方面来讲,错误通常是语法或逻辑上的。语法错误会导致程序代码不能被解释器解释,这些错误必须在程序执行前纠正。当程序的语法正确后,剩下的就是逻辑错误了。逻辑错误可能是由于不完整或是不合法的代码逻辑所致,还可能是由于代码逻辑无法生成或执行。

编译时会检查语法错误,编译完成后 Python 解释器会在程序运行时检测逻辑错误。当检测到一个错误,Python 解释器就引发一个异常,并显示异常的详细信息。

  • 语法错误
>>> if

  File "<stdin>", line 1
    if
     ^
SyntaxError: invalid syntax

程序执行过程中,Python 解释器会检测你的程序是否存在语法错误,如果程序出错,Python 解析器会复现语法错误的那行代码,并用一个小 “箭头” 指向行里检测到的第一个错误。

  • 逻辑错误
>>> 1/0

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

在生活中 0 是不能作为被除数的,程序写的语法可能没问题,但是逻辑上不一定没有问题,这就是一种逻辑错误。

异常

即使语句或表达式在语法上是正确的,但在尝试执行时,它仍可能会引发错误。在执行时检测到的错误被称为异常

大多数异常并不会被程序处理,而是以错误信息的方式显示出来。

>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined

>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

错误信息的最后一行告诉我们程序遇到了什么类型的错误。异常有不同的类型,而其类型名称将会作为错误信息的一部分中打印出来。

异常的步骤:

  • 异常产生,检查到错误且解释器认为是异常,抛出异常
  • 异常处理,截获异常,系统忽略或者终止程序处理异常

Python 中的异常类

在 Python 中不同的异常可以用不同的类型去标识,一个异常标识一种错误。Python 中所有的错误都是从 BaseException 类派生的。

序号 异常名称 描述
1 BaseException 所有异常的基类
2 Exception 常规错误的基类
3 NameError 未声明/未初始化对象
4 ZeroDivisionError 除数为零
5 SyntaxError Python 解释器语法错误
6 IndexError 请求的索引超出序列范围
7 KeyError 请求一个不存在的字典关键字
8 IOError 输入/输出错误
9 AttributeError 对象没有这个属性
10 ImportError 导入模块/对象失败
11 TypeError 对类型无效的操作

检测和处理异常

异常可以通过 try 语句来检测,任何在 try 语句块里的代码都会被监测, 检查有无异常发生。try 语句有两种主要形式:

  • try-except
  • try-finally

这两个语句是互斥的, , 也就是说你只能使用其中的一种。一个 try 语句可以对应一个或多个 except 子句,但只能对应一个 finally 子句,或是一个 try-except-finally 复合语句。可以使用 try-except 语句检测和处理异常,也可以添加一个可选的 else 子句处理没有探测到异常时执行的代码,而 try-finally 只允许检测异常并做一些必要的清除工作(无论发生错误与否)。

try/except

最 常 见 的 try-except 语 句 语 法 如 下 所 示,它 由 try 块 和 except 块组成,也可以有一个可选的错误原因except_01

try 语句按照如下方式工作:

  • 首先,执行 try 子句
  • 如果没有异常发生,则忽略 except 子句,try 子句执行后结束
  • 如果在执行 try 子句的过程中发生了异常,那么 try 子句余下的部分将被忽略。如果异常的类型和 except 之后的名称相符,那么对应的 except 子句将被执行。
  • 如果一个异常没有与任何的 except 匹配,那么这个异常将会传递给上层的 try
while True:
    try:
        x = int(input("Please input a number: "))
        break
    except ValueError:
        print("That was no valid number. Please try again...")

Please input a number: s
That was no valid number. Please try again...

try/except...else

try/except 语句还有一个可选的 else 子句,如果使用这个子句,那么必须放在所有的 except 子句之后。

else 子句将在 try 子句没有发生任何异常的时候执行。

except_02

以下实例在 try 语句中判断文件是否可以打开,如果打开文件时是正常的没有发生异常,则执行 else 部分的语句,读取文件内容。

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print("cannot open file '{}'".format(arg))
    else:
        print("'{}' has {} lines".format(arg,len(f.readlines())))
        f.close()


cannot open file '-f'
'/Users/allen/Library/Jupyter/runtime/kernel-2ce1159e-715f-4571-982a-b0c3c78c123f.json' has 12 lines        

使用 else 子句比把所有的语句都放在 try 子句里面要好,这样可以避免一些意想不到,而 except 又无法捕获的异常。

异常处理并不仅仅处理那些直接发生在 try 子句中的异常,而且还能处理子句中调用的函数(甚至间接调用的函数)里抛出的异常。

def this_fails():
    x = 1/0

try:
    this_fails()
except ZeroDivisionError as err:
    print('Handling run-time error:', err)


Handling run-time error: division by zero    

try-finally

try-finally 语句无论是否发生异常都将执行最后的代码。

except_03

以下实例中 finally 语句无论是否发生异常都会执行。

try:
    runoob()
except AssertionError as error:
    print(error)
else:
    try:
        with open('file.log') as file:
            read_data = file.read()
    except FileNotFoundError as fnf_error:
        print(fnf_error)
finally:
    print('这句话,无论异常是否发生都会执行...')


这句话,无论异常是否发生都会执行...
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-7-4eb8e067a908> in <module>
      1 try:
----> 2     runoob()
      3 except AssertionError as error:
      4     print(error)
      5 else:

NameError: name 'runoob' is not defined    

处理多个异常

  • 把多个 except 语句连接在一起, 处理一个 try 块中可能发生的多种异常。
try:
    try_suite
except Exception1:
    suite_for_exception_Exception1
except Exception2:
    suite_for_exception_Exception2

程序首先尝试执行 try 子句,如果没有错误,忽略所有的 except 子句继续执行。如果发生异常,解释器将在这一串处理器(except 子句)中查找匹配的异常,如果找到对应的处理器,执行流将跳转到这里。

Python 支持把 except 语句串连使用,分别为每个异常类型分别创建对应的错误信息,这样用户可以得到更详细的关于错误的信息。

def safe_float(obj):
    try:
        retval = float(obj)
    except ValueError:
        retval = 'Could not convert non-number to float...'
    except TypeError:
        retval = 'Object type cannot be converted to float...'
    return retval


safe_float('opcoder')
'Could not convert non-number to float...'

safe_float(['1','2'])
'Object type cannot be converted to float...'
  • except 语句可以同时处理任意多个异常,但要求异常被放在一个元组里。
def safe_float(obj):
    try:
        retval = float(obj)
    except (ValueError, TypeError):
        retval = 'Argument must be a number or numeric string...'
    return retval


safe_float('opcoder')
'Argument must be a number or numeric string...'

safe_float(['1','2'])
'Argument must be a number or numeric string...'

捕获所有异常

  • 不存储异常描述信息
try:
    f = open('myfile.txt', 'r')
except:
    print('Raise a exception...')


Raise a exception...    
  • 存储异常描述信息
try:
    f = open('myfile.txt', 'r')
except Exception as e:
    print('Raise a exception: ', e)


Raise a exception:  [Errno 2] No such file or directory: 'myfile.txt'

异常的参数

异常也可以有参数,异常引发后它会被传递给异常处理器。当异常被引发后,参数是作为附加帮助信息传递给异常处理器的

把这个参数放在 except 语句后,接在要处理的异常后面。

# single exception
except Exception as reason:
    suite_for_Exception_with_Argument

# multiple exceptions
except (Exception1, Exception2, ..., ExceptionN) as reason:
    suite_for_Exception1_to_ExceptionN_with_Argument
def safe_float(obj):
    try:
        retval = float(obj)
    except (ValueError, TypeError) as err:
        retval = err
    return retval


safe_float('opcoder')
ValueError("could not convert string to float: 'opcoder'")

safe_float(['1','2'])
TypeError("float() argument must be a string or a number, not 'list'")

抛出异常

Python 允许程序自行引发异常,使用 raise 语句即可。

异常是一种很 “主观” 的说法,以下雨为例,假设大家约好明天去爬山郊游,如果第二天下雨了,这种情况会打破既定计划,就属于一种异常;但对于正在期盼天降甘霖的农民而言,如果第二天下雨了,他们正好随雨追肥,这就完全正常。

很多时候,系统是否要引发异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序员来决定引发,系统无法引发这种异常。

如果需要在程序中自行引发异常,则应使用 raise 语句,语法格式如下:

raise [Exception [, args [, traceback]]]

except_04

以下实例如果 x 大于 5 就触发异常:

x = 10
if x > 5:
    raise Exception('x 不能大于 5。x 的值为: {}'.format(x))

执行以上代码会触发异常:

Exception                                 Traceback (most recent call last)
<ipython-input-27-e00bf68fb93c> in <module>
      1 x = 10
      2 if x > 5:
----> 3     raise Exception('x 不能大于 5。x 的值为: {}'.format(x))

Exception: x 不能大于 5x 的值为: 10

raise 唯一的一个参数指定了要被抛出的异常。它必须是一个异常的实例或者是异常的类(也就是 Exception 的子类)。

如果你只想知道这是否抛出了一个异常,并不想去处理它,那么一个简单的 raise 语句就可以再次把它抛出。

try:
    raise NameError('HiThere')
except NameError:
    print('An exception flew by!')
    raise


An exception flew by!
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-28-bf6ef4926f8c> in <module>
      1 try:
----> 2     raise NameError('HiThere')
      3 except NameError:
      4     print('An exception flew by!')
      5     raise

NameError: HiThere

用户自定义异常

在实际开发中,有时候系统提供的异常类型不能满足开发的需求。这时候你可以通过创建一个新的异常类来拥有自己的异常。异常类继承自 Exception 类,可以直接继承,或者间接继承。

自定义异常类型

用户自定义异常类型,只要该类继承了 Exception 类即可,至于类的主体内容用户自定义,可参考官方异常类。

class InputError(Exception):    
    def __init__(self, errorinfo):
        self.error = errorinfo
    def __str__(self):
        return self.error

手动抛出自定义异常

系统的自带的异常只要触发就会自动抛出,比如 NameError,但用户自定义的异常需要用户自己决定什么时候抛出。

raise 唯一的一个参数指定了要被抛出的异常。它必须是一个异常的实例或者是异常的类(也就是 Exception 的子类)。大多数的异常的名字都以 "Error" 结尾,所以实际命名时尽量跟标准的异常命名一样。

# 1. 用户自定义异常类型
class InputError(Exception):    
    def __init__(self, errorinfo):
        self.error = errorinfo  
    def __str__(self):
        return self.error

# 2. 手动抛出用户自定义类型异常     
def score():
    grade = int(input('你的成绩是: '))
    if grade <= 0 or grade >= 150:
        raise InputError('考试分数只能在0~150')

score()       


你的成绩是: 200
---------------------------------------------------------------------------
InputError                                Traceback (most recent call last)
<ipython-input-50-1a3aa26f3a2b> in <module>
     10         raise InputError('考试分数只能在0~150')
     11 
---> 12 score()

<ipython-input-50-1a3aa26f3a2b> in score()
      8     grade = int(input('你的成绩是: '))
      9     if grade <= 0 or grade >= 150:
---> 10         raise InputError('考试分数只能在0~150')
     11 
     12 score()

InputError: 考试分数只能在0~150

捕捉手动抛出的自定义异常

# 1. 用户自定义异常类型
class InputError(Exception):    
    def __init__(self, errorinfo):
        self.error = errorinfo   
    def __str__(self):
        return self.error

# 2. 手动抛出用户自定义类型异常     
def score():
    grade = int(input('你的成绩是: '))
    if grade <= 0 or grade >= 150:
        raise InputError('考试分数只能在0~150')

try:
    score()
except InputError as e:
    print("捕捉到异常了...")
    print("打印异常信息: ",e)


你的成绩是: 200
捕捉到异常了...
打印异常信息:  考试分数只能在0~150    

定义清理行为

try 语句还有另外一个可选的子句,它定义了无论在任何情况下都会执行的清理行为。

try:
    raise KeyboardInterrupt
finally:
    print('Goodbye, world!')


Goodbye, world!
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-55-ca8991ac7661> in <module>
      1 try:
----> 2     raise KeyboardInterrupt
      3 finally:
      4     print('Goodbye, world!')

KeyboardInterrupt:     

以上例子不管 try 子句里面有没有发生异常,finally 子句都会执行。

如果一个异常在 try 子句里(或者在 except 和 else 子句里)被抛出,而又没有任何的 except 把它截住,那么这个异常会在 finally 子句执行后被抛出。

下面是一个更加复杂的例子,在同一个 try 语句里包含 exceptfinally 子句。

def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("division by zero!")
    else:
        print("result is", result)
    finally:
        print("executing finally clause")

>>> divide(2, 1)        
result is 2.0
executing finally clause

>>> divide(2, 0)   
division by zero!
executing finally clause

预定义的清理行为

一些对象定义了标准的清理行为,无论系统是否成功的使用了它,一旦不需要它了,那么这个标准的清理行为就会执行。

这面这个例子展示了尝试打开一个文件,然后把内容打印到屏幕上。

for line in open("myfile.txt"):
    print(line, end="")

以上这段代码的问题是,当执行完毕后,文件会保持打开状态,并没有被关闭。

关键词 with 语句就可以保证诸如文件之类的对象在使用完之后一定会正确的执行他的清理方法。

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

以上这段代码执行完毕后,就算在处理过程中出问题了,文件 f 总是会关闭。

断言

assert 翻译成中文就是「断言」的意思,它是一句等价于布尔真的判断,如果它发生异常的话,意味着表达式为假。

except_05

首先让我们先来看点简单的代码,从代码中理解 assert

>>> assert 'a' == 'a'

>>> assert 'a' == 'b'
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-63-153cc9b77bcd> in <module>
----> 1 assert 'a' == 'b'

AssertionError: 

assert 的应用场景很像它在汉语中的意思: 当程序运行到某个节点的时候,就断定某个变量的值必然是什么,或者是对象必然拥有某个属性等。简单点来说的话,就是断定是什么东西就必然是什么东西,如果不是,就抛出异常。

接下来我们再来看一段稍微复杂点的代码:

class Account(object):
    def __init__(self,num):
        self.number = num
        self.cnt = 0

    def deposit(self,amount):
        try:
            assert amount > 0
            self.cnt += amount
        except:
            print('the amount can not be zero.')

    def withdraw(self,amount):
        assert amount > 0
        if amount <= self.cnt:
            self.cnt -= amount
        else:
            print('cnt is not enough')

在上面的代码中,我们设置的是 deposit()withdraw() 方法的参数 amount 必须大于零,这里用的就是断言,如果不满足条件的话就会直接报错。比如像下面这样来运行:

if __name__ == "__main__":
    a = Account(100)
    a.deposit(-10)

出现的结果如下所示:

the amount can not be zero.

以下实例判断当前系统是否为 Linux,如果不满足条件则直接触发异常,不必执行接下来的代码:

import sys
assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"

# 接下来要执行的代码

这就是断言 assert 的应用。

参考资料

Python3 错误和异常

Python3 用户自定义异常

Python3 断言

原创文章,转载请注明出处:http://www.opcoder.cn/article/40/