Python 中的异常处理

Author Avatar
Tr0y 3月 27, 2019 09:03:16 本文共 4k 字
  • 文为知己者书
  • 在其它设备中阅读本文章

Python 用于处理异常一共有 4 个关键字,分别是 try、except、else、finally,该怎么用呢?

为什么要有 try

假如你是 Python 的开发者,刚开始的时候你很高兴地发现自己创造的语言很棒,并为此感到自豪。有一天,你写了一个函数,作用是 5 分钟爬一次电影网站,并将结果存到文件里:

while 1: 
    json = spider('www.tr0y.wang')
    save(json, 'result.json')
    time.sleep(5*60) # 5min 爬一次

spider 就是爬取页面的爬虫,save 是保存 json 格式的结果到 result.json 里。

有一天,你发现网络环境不稳定的时候,spider 函数会报错:timeout。一旦出错,程序就会报错退出:

NetworkError: timeout!

网络波动导致 timeout 的话,多试一次不就行了?于是,你发明了 try 关键字用于捕获异常,出现异常则跳过 try 块里剩下的语句。这样就可以愉快地重试了:

while 1:
    for i in range(10): # 尝试 10 次
        try:
            json = spider('www.tr0y.wang')
            save(json, 'result.json')
            break # 成功则退出
        time.sleep(60) # 歇口气再试
    time.sleep(5*60) # 5min 爬一次

又过了一段时间,你发现爬虫虽然不会因为 timeout 报错而终止了,但是又出现了新的报错:

DecodeError: Json format error!

经过一番 debug,你发现了由于这个网站的服务器相当土豆,请求人数太多的时候反应不过来会导致 500 的 http status。这时返回的是非 json 格式的结果,而 spider 函数会将返回的结果当做 json 格式来处理,拿到的数据是非 json 格式的自然会导致 spider 函数报错。这个时候,应当等待更长的时间再爬。那么问题来了,如何区分不同的报错并进行处理呢?只用 try 关键字的话肯定是没法解决了,毕竟 try 没有区分异常的功能。

except 抵达战场

为了解决这个问题,你发明了 except 关键字,能够根据不同的异常类捕获不同的报错,并且报错的信息通过 as 赋给变量,比如下面就是赋给变量 e

while 1:
    for i in range(10): # 尝试 10 次
        try:
            json = spider('www.tr0y.wang')
            save(json, 'result.json')
            break # 成功则退出
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试

    time.sleep(5*60) # 5min 爬一次

如果 except 后面没加具体的异常,则就和之前的第一版 try 一样,捕获所有错误。并且多个异常可以进行合并:

try:
    # do something wrong
except (ValueError, IndexError) as e:
    print(e)

这样代表捕获 ValueErrorIndexError时都进行print(e)

这下就解决了上面的问题。舒服!

解决了 json 获取的问题,接下来,你写了各种函数用于解析、展示、存储 json 文件:

while 1:
    for i in range(10): # 尝试 10 次
        try:
            json = spider('www.tr0y.wang')
            save(json, 'result.json')

            # 新增的函数
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试

    time.sleep(5*60) # 5min 爬一次

else 的诞生

随着处理函数的增加,try 语句块越来越长…直到有一天,有人在 github 看到了你的代码,费了半天的劲才明白你的 try 到底是在捕获哪个语句可能出现的错误,并顺手在 issue 留下了评论:

Your `try` is so 
l
o
o
o
o
o
o
o
o
o
o
ng

你焕然大悟,在 try 里堆砌过多的语句将导致 try 可读性降低。再说了,try 块里的语句的异常,并不是你都想捕获的。于是你灵机一动,那我设置一个 flag 好了:

while 1:
    for i in range(10): # 尝试 10 次
        error = 1
        try:
            json = spider('www.tr0y.wang')
            error = 0
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试

        if not error: # 说明没报错
            save(json, 'result.json')
             # 新增的函数
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
    time.sleep(5*60) # 5min 爬一次

问题解决了,但是怎么看怎么不顺眼,作为一个牛逼的语言发明者,这样简单的语法需求都没支持,还得每次都让开发者设置 flag,太不(mei)优(bi)雅(ge)了。于是你大手一挥,又发明了 else 关键字,配合 try 在 try 块没报错的情况下被执行:

while 1:
    for i in range(10): # 尝试 10 次
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')

            # 新增的函数
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
    time.sleep(5*60) # 5min 爬一次

爬虫从此稳定得一批。

finally:Python 的简洁

又双过了一段时间,由于没有时间记录,你不知道每次爬取尝试运行了多久。于是你补充了几个语句,用于计算每次尝试执行的时间:

while 1:
    for i in range(10): # 尝试 10 次
        start = time.time() # 起始时间点
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
            end = time.time() # 结束时间点
            print(end-start, 's')
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试
            end = time.time() # 结束时间点
            print(end-start, 's')
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            end = time.time() # 结束时间点
            print(end-start, 's')
            break # 成功则退出
    time.sleep(5*60) # 5min 爬一次

看上去挺好的,但是有大量的重复语句,能不能简洁一点呢?当然可以,你瞬间想起了用函数来封装记录时间的语句。但是只不过是把 n 个重复的语句变成 1 个重复语句罢了,还是不够简洁。于是灵机一动,调整了一下语句的位置:

while 1:
    for i in range(10): # 尝试 10 次
        start = time.time() # 起始时间点
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
        # 总是需要执行的代码放这里
        end = time.time() # 结束时间点
        print(end-start, 's')
    time.sleep(5*60) # 5min 爬一次

看上去不错,简洁了很多,但是实际上有个 bug,就是如果执行成功后,是不会执行记录时间的语句的,因为 else 里有 break,直接跳过了记录时间的语句。
于是,你又发明了 finally 语句,不管 try 的语句块执行报不报错,都会被执行,而且吸收了之前的教训,try、except、else 的语句中的 break、continue、return 语句不会跳过 finally 块里的语句,甚至连它们执行时出现的未捕获异常,也要在执行完 finally 块后才会抛出。
于是,代码就成为了:

while 1:
    for i in range(10): # 尝试 10 次
        start = time.time() # 起始时间点
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            time.sleep(3*60) # 歇一大口气再试
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
        finally: # 就算 else 里 break 也会执行 finally
            # 总是需要执行的代码放这里
            end = time.time() # 结束时间点
            print(end-start, 's')
    time.sleep(5*60) # 5min 爬一次

raise 异常制造机

叒叕叒叕过了一段时间,你发现这个网站的服务器变得更渣了,一旦人数过多崩了,第二天才会恢复。这时候之前的 time.sleep(3*60) # 歇一大口气再试 意义也不大了,还不如停下来,等第二天手动恢复。但是你又想记录这个状态,这就意味着不能简单地把 except DecodeError as e: 删了,因为还要打印呢。于是你灵机一动,将代码改为:

while 1:
    for i in range(10): # 尝试 10 次
        start = time.time() # 起始时间点
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            # time.sleep(3*60) # 歇一大口气再试
            kill
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
        finally: # 就算 else 里 break 也会执行 finally
            # 总是需要执行的代码放这里
            end = time.time() # 结束时间点
            print(end-start, 's')
    time.sleep(5*60) # 5min 爬一次

由于变量 kill 没有定义,代码便在进入except DecodeError as e:后,print完就报错退出了。这显然是一个骚操作(我之前就是这么干的 :P)。于是你又发明了 raise关键字用于触发报错:

In [3]: raise RuntimeError('123')
Traceback (most recent call last):
  File "<ipython-input-3-81afeb2aaf23>", line 1, in <module>
    raise RuntimeError('123')
RuntimeError: 123

raise 后面可以加上异常的类型,也可以不加,不加的话则重新抛出捕获的异常:

In [4]: try:
   ...:     1/0
   ...: except ZeroDivisionError:
   ...:     raise # 重新抛出 ZeroDivisionError
   ...:
Traceback (most recent call last):
  File "<ipython-input-4-2791351c601e>", line 2, in <module>
    1/0
ZeroDivisionError: division by zero

最最最最最后,代码就成了:

while 1:
    for i in range(10): # 尝试 10 次
        start = time.time() # 起始时间点
        try:
            json = spider('www.tr0y.wang')
        except NetworkError as e:
            print('网络波动:', e)
            time.sleep(60) # 歇口气再试
        except DecodeError as e:
            print('服务器扛不住了:', e)
            # time.sleep(3*60) # 歇一大口气再试
            raise RuntimeError('服务器扛不住了,需要等第二天手动重启!')
        else: # 负责帮助 try 块执行后续命令
            save(json, 'result.json')
            analysis('result.json')
            ... # 省略 n 个函数
            show('result.json')
            break # 成功则退出
        finally: # 就算 else 里 break 也会执行 finally
            # 总是需要执行的代码放这里
            end = time.time() # 结束时间点
            print(end-start, 's')
    time.sleep(5*60) # 5min 爬一次

其他细节

最后再说一下细节:
如果没有 try 关键字,那么使用后三个关键字是没有意义的(Python 的语法也不允许)。那么我们自然而然会想到以下组合:

  1. try + except:如果出错,执行 except 块里的语句
  2. try + except + else:如果出错,执行 except 块里的语句,否则执行 else 里的语句
  3. try + except + else + finally:如果出错,执行 except 块里的语句,否则执行 else 里的语句,最后不管有没有出错,都执行 finally 里的语句
  4. try + else:如果没出错,执行 else 块里的语句(本质上是忽略所有异常)
  5. try + else + finally:如果没出错,执行 else 块里的语句,最后不管有没有出错,都执行 finally 里的语句(本质上是忽略所有异常)
  6. try + finally:最后不管有没有出错,都执行 finally 里的语句

如果你去试,你会发现第 4、5 点是不行的,会报语法错误。为什么不设计 try-else 的语法呢?我们看一下怎么使用 try-except-else 来实现我们想要的 try-else:
效果:

try:
    may be wrong
else:
    do something...

实现:

try:
    may be wrong
except:
    pass
else:
    do something...

有些人可能觉得第一种很简洁。但是第二种写法很清晰地传达了一个意思:捕获并忽略所有异常,比较符合 Python 之禅的中的:explicit is better than implicit。至于采用哪种更好,Guido 说了算 :P

最后,既然提到了“忽略所有异常”,再说一下 except 后写不写 Exception 的区别吧:

try:
  do something
except: 
  pass

try:
  do something
except Exception: 
  pass

捕获异常到底怎么写?except: 还是 except Exception, e: 还是 except Exception as e: ?

首先,except Exception, e: 已经是很旧很旧的写法了,还不兼容 3.x。并且有时候会坑死你(以下代码当然只在 2.x 运行):

try:
    1/0
except ZeroDivisionError, IndexError:
    print('You are WRONG!')

乍一看这段代码的作用是捕获 ZeroDivisionErrorIndexError 2 个异常,来试试:

In [1]: try:
   ...:     1/0
   ...: except ZeroDivisionError, IndexError:
   ...:     print('You are WRONG!')
   ...:
You are WRONG!

In [2]: try:
   ...:     [][0]
   ...: except ZeroDivisionError, IndexError:
   ...:     print('You are WRONG!')
   ...:
---------------------------------------------------------------------------
Traceback (most recent call last)
<ipython-input-2-69cefe22dccf> in <module>()
      1 try:
----> 2     [][0]
      3 except ZeroDivisionError, IndexError:
      4     print('You are WRONG!')
      5

IndexError: list index out of range

IndexError 居然没被捕获,那 IndexError 是拿去干嘛用了呢?实际上充当了之前变量 e 的作用:

In [4]: try:
   ...:     1/0
   ...: except ZeroDivisionError, IndexError:
   ...:     print(IndexError)
   ...:
integer division or modulo by zero

巨大的深坑 :)

所以,except Exception, e: 先不考虑。

except:except Exception as e: 的区别在于,异常的继承的类型不同。前者捕获的异常涵盖了直接、间接继承 BaseException 的所有异常;而后者只包括了直接、间接继承 Exception 的异常。

我们知道,要想捕获哪一种异常,就在 except 后面加上这个异常类。那么 Exception 到底指的是哪个异常呢?
下面是异常的继承的关系图:

BaseException # 前者
├── SystemExit 
├── KeyboardInterrupt
├── GeneratorExit
└── Exception # 后者
    ├── StopIteration 
    ├── ArithmeticError 
    │     ├── ZeroDivisionError 
    │     └── ... # 省略了
    ├── AssertionError 
    ├── AttributeError
    ├── ImportError 
    ├── IndexError
    └── ... # 省略了

完整的这里有:传送门🚪

可以看到,前者包含的 SystemExit、KeyboardInterrupt、GeneratorExit 是后者没有的。所以前者捕获的异常更加彻底,包括如键盘中断程序退出请求(用 sys.exit() 就无法退出程序了,因为异常被捕获了)。这三个异常不推荐随意捕获,因为它们被认为是比较严重的异常。举例:

import sys

while 1:
    try:
        while 1: pass
    except:
        print sys.exc_info()[2]

按下 ctrl+c 也不会中断:

而这样就可以:

import sys

while 1:
    try:
        while 1: pass
    except Exception as e:
        print sys.exc_info()[2]

最后,在 except Exception as e: 中,e 作为 Exception 的一个实例,会有很多用处。比如,当你在使用 urllib 抓取一个页面的时候,服务器返回 500。这个时候 Python 会报错,但是页面却有返回内容,而你又想拿到这个页面的内容时,就可以在异常中获取:

import urllib.request
from urllib.error import HTTPError

result = urllib.request.Request(url="xxx")
try:
    handler = urllib.request.urlopen(result)
    handler.read() # 由于 500 会报错,所以代码不会走到这里
except HTTPError as e:
    content = e.read() # 输出 500 页面的内容

所以,对于 except Exception 来说,Exception 指的是 Exception 异常类,所有直接、间接继承 Exception 的异常都会被捕获。若不写具体的异常类,则视为 BaseException,即 except:except BaseException: 是一样的~

总结

总结一下,异常捕获、处理的流程是这样的:

即:

  1. try 块:可能出现错误的语句
  2. except:出错时的处理语句
  3. else:没出错的时候继续执行的语句
  4. finally:不管有没有出错,都会执行的语句

最后还有个 raise:用于(重新)抛出异常

End

What do you think?

本文标题: Python 中的异常处理
原始链接: http://www.tr0y.wang/2019/03/27/Python异常处理/
发布时间: 2019.03.27-09:03
最后更新: 2019.04.21-16:31
版权声明: 本站文章均采用CC BY-NC-SA 4.0协议进行许可。转载请注明出处!