20. 异常处理

cover.png

Hi,大家好。我是茶桁。

在我们日常使用Python或者其他编程语言的时候,不可避免的都会出现报错和异常。那么,我们今天就来谈谈异常。

什么是异常?

异常异常,根据名字简单理解,那就是非正常,也就是没有达到预期目标。

异常呢,其实就是一个事件,并且这个异常事件在程序的运行过程中出现,会影响程序的正常执行。而一般来说,异常被分为两种:

  1. 语法错误导致的异常
  2. 逻辑错误导致的异常

比如:

varlist = [1, 2, 3]
print(varlist[3])

---
IndexError: list index out of range

这个时候,系统抛出了异常,提示我们列表索引超出范畴。

这里我们需要知道,「异常」在Python中实际上也是一个对象,表示一个错误。当我们的程序无法继续正常进行时,就会被抛出。

我们来完整的看看这个报错信息:

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[2], line 2
      1 varlist = [1, 2, 3]
----> 2 print(varlist[3])

IndexError: list index out of range

Python在遇到异常之后,首先会给出一个「错误回溯」, 然后给出具体哪一句代码出现了问题。

然后在最后给出异常分类和解释。那么IndexError告知我们,这是一个「索引错误」,并且给出了具体的描述「列出索引超出范围」。其中IndexError是我们的异常类, list index out of range是我们的异常信息。

在程序运行过程中,会出现各种各样的异常类,常见标准异常类,我放在最下面作为一个附录。

如何处理异常

可预知

如果错误发生的情况是我们可以预知的,那么就可以使用流程控制进行预防处理。比如,两个数字的运算,其中一个不是数字,运算就会出错,这个时候就可以判断来预防:

n2 = '3'
if isinstance(n2, int):
    res = 10+n2
    print(res)
else:
    print('非整型。')

---
非整形

在这一段代码中,我们使用isinstance方法来检测第一个参数是否是第二个参数的所属类型。这是一个用来检测的方法,返回True或者False。那我们在if中,只有真才会打印结果,假则会打印另外一则消息。

有些小伙伴会想,那既然知道不是整型就会出错,那前面限制传如整型不就好了,干嘛还要费劲去做非整判断。

你要知道,很多时候一个程序的编写和维护并不是单一一个人来做的,即便是一个人在做,也不能完全保证自己某个地方埋下了隐患。那么在每一段代码中,我们对可能预知的情况做妥善的预防是必须的。

不可预知

那可预知的情况我们避免了,可是在我们编写代码的时候,更多的情况是我们自己都不知道我们到底埋了什么雷,哪一段没有遵循规则或者逻辑。那这种情况就是不可预知的。

对于这种不可预知的情况我们该怎么办呢?我们又没办法预先判断。那这种情况下,我们可以使用try...except...语句,在错误发生时进行处理。相关语法如下:

try:
    可能发生异常错误的代码
except:
    如果发生异常这进入except代码块进行处理

异常被捕获之后程序继续向下执行

我们来看个示例,比如我们之前做过的一个注册、登录练习。其中我们有一段代码是要去读取列表中的所有用户。之前我们的练习中,有提到过文件不存在的情况,所以我们使用了a+的方法,当文件不存在的时候,就新建。

那么现在,我们假设我们就用了r的方法,当文件不存在的时候,一定会报错对吧?这个时候,我们可以使用两种方式来进行处理。

第一种方式,就可以在读取前先判断当前文件是否存在。

第二种方式,就可以使用try...except...在错误发生的时候进行处理。

那么这里,我们用第二种方式来做一下处理:

# 假设我们读取的文件不存在,会发生错误
try:
    with open('./data/user5.txt', 'r') as fp:
        res = fp.read()
    print(res)
except:
    print('文件不存在。')

print('程序继续运行...')

---
文件不存在。
程序继续运行...

可以看到,我们准确的捕获了错误,并且之后程序仍然继续往后执行了。

⚠️ try...except... 是在错误发生后进行处理,并不是提前判断。也就是说,错误其实还是发生了。这和if实际上有根本性的区别。

try...except... 详解

首先,我们认识try...except的一个特性,就是它可以处理指定的异常,如果引发了非指定的异常,则无法处理。比如,我们下面人为制造一个异常:

s1 = 'hello'
int(s1)

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

可以看到,我们这一段代码引发了一个ValueError异常。

现在我们来捕获一下, 但是这次,我们为这个异常指定一个异常类再来看看,先看看正常状态下:

try:
    s1 = 'hello'
    int(s1)
except:
    print('程序错误。')
    
---
程序错误。

接着我们来看指定异常之后:

try:
    s1 = 'hello'
    int(s1)
except IndexError as e:
    print('程序错误。')
    
---
ValueError: invalid literal for int() with base 10: 'hello'

这里我们指定了一个IndexError的异常类,显然我们之前看到了,程序报错是ValueError异常类,两者并不匹配。所以最后依然还是报错。

那么之前我们谈到过标准的异常类,并且也知道异常实际上也就是一个对象。而我们平时在使用的时候,except实际上就是去这个「标准的异常类」的列表里去查找,如果没有对应的异常类,它依然是无法捕获的。不过大部分时候,我们基本不会遇到标准异常类之外的异常。而这种处理指定的异常类的特性,平时也可以被我们使用。

其中一个使用方式,就是进行多分支处理异常类,不同的异常可以走不通的except进行处理:

s1 = 'hello'
try:
    s1[5] # IndexError
except IndexError as e:
    print('这里是IndexError', e)
except KeyError as e:
    print('这里是KeyError', e)
except ValueError as e:
    print('这里是ValueError', e)
    
---
这里是IndexError string index out of range

是不是和if...elif的分支形式很像?

让我们继续,在我们说指定的异常类中,实际上会有一个万能的通用异常类。那就是Exception

s1 = 'world'
try:
    int(s1)
except Exception as e:
    print('Exception ===',e)
    
---
Exception === invalid literal for int() with base 10: 'world'

基本上所有的异常,都可以走到这个异常类。在这段代码中,我们之前记得int(s1)是属于一个ValueError, 但是我们使用Exception依然可以获取到这个错误。可是如果这两种异常类同时被指定的情况下会如何?

s1 = 'world'
try:
    int(s1)
except Exception as e:
    print('Exception ===',e)
except ValueError as e:
    print('ValueError ===', e)
    
---
Exception === invalid literal for int() with base 10: 'world'

我们看到,就是按照程序的从上至下的顺序在执行。

所以,其实我们可以这样理解,当我们进行多分支异常类+通用异常类的时候,Exception是最后的一个保底。

s1 = 'hello'
try:
    # int(s1) # ValueError
    s1[5]    # IndexError
except IndexError as e:
    print('IndexError',e)
except KeyError as e:
    print('KeyError',e)
except ValueError as e:
    print('ValueError',e)
except Exception as e:
    print('Exception',e)
    
---
IndexError string index out of range

除此之外,try...except是支持else的,当try里的代码顺利执行没有捕获到任何错误之后,还可以走到else之中额外执行分支内的代码:

s1 = 'hello'
try:
    str(s1)
    print(s1)
except IndexError as e:
    print('IndexError',e)
except ValueError as e:
    print('ValueError',e)
except Exception as e:
    print('Exception',e)
else:
    print('try代码块中没有引发异常时,执行')
    
---
hello
try代码块中没有引发异常时,执行

我们再来了解一下finally, 这个方法是无论是否引发异常都会执行。通常情况下用于执行一些清理工作:

s1 = 'hello'
try:
    int(s1)
    print('如果前面的代码引发了异常,这个代码块将不在继续执行。。')
except IndexError as e:
    print('IndexError',e)
except ValueError as e:
    print('ValueError',e)
except Exception as e:
    print('Exception',e)
else:
    print('try代码块中没有引发异常时,执行')
finally:
    print('无论是否引发了异常,都会执行这个代码块')

print('如果上面的代码有异常并且进行了处理,那么后面的代码将继续执行')

---
ValueError invalid literal for int() with base 10: 'hello'
无论是否引发了异常,都会执行这个代码块
如果上面的代码有异常并且进行了处理,那么后面的代码将继续执行

这段代码中,我们引发了一个异常,也被捕获了。但是依然执行了finally内的代码,并且也未影响程序继续往后执行。

在我们平常写代码的过程中还有一种情况,就是我们需要自己制作一个异常信息,然后抛出。这个时候,我们需要用raise, 来主动抛出异常。

try:
    raise Exception('发生错误')
except Exception as e:
    print('Exception', e)
    
---
Exception 发生错误

除了上述的异常处理之外,其实还有另外一种方式,是直接判断逻辑是否成立,不成立抛出AssertionError错误。就是使用assert进行断言。它在表达式错误的时候,会直接抛出AssertionError错误,如果表达式正确,这什么都不做。

assert 2 > 3

---
AssertionError: 

自定义异常处理类

虽然系统已经给到了很多异常处理的方式,而我们在平时开发中也会经常的使用。但是实际上,很多时候我们都需要一些自己的处理要求。比如说,当异常出现的时候,我们要将异常信息写入日志,在日后我们从日志里查看日常信息或者做数据分析,就是我们最常使用的。

那我们接下来看看,如果做一个异常处理的自定义开发:

再最开始,我们需要归纳一下,我们到底要保存怎样一个格式:

# 日志的基本格式:
- 日期时间, 异常的级别
- 异常信息:引发的异常类别,异常的信息,文件及行号。

在确定了日志格式后,我们可以进入开发了,首先我们需要导入两个所需的库

# 先导入所需模块
import traceback
import logging

让我们先来人为创建一个日常,并用try语句来捕获它:

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

这句代码报了一个ValueError异常类。

try:
    int('aaa')
except:
    print('在此进行异常的处理')
    
---
在此进行异常的处理

没问题,我们捕获了异常并且正确的进入了except。那么,我们可以通过traceback模块来获取异常信息, 替换一下打印信息我们来查看一下。

try:
    int('aaa')
except:
    # 通过traceback获取异常信息
    errormsg = traceback.format_exc()
    print(errormsg)
    
---
Traceback (most recent call last):
  File "/var/folders/h4/7cr1cmpn7v5b3x20_9wz8m740000gn/T/ipykernel_39689/2534911191.py", line 2, in <module>
    int('aaa')
ValueError: invalid literal for int() with base 10: 'aaa'

接下来,就轮到logging模块了。该模块定义了实现用于应用程序和库的灵活事件日志记录系统的函数和类。

logging.basicConfig(
    filename = './data/error.log', # 日志存储的文件及目录
    format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
    datefmt = '%Y-%m-%d %H:%M:%S'
)

在定义了logging的基本信息之后,我们就可以定义一下将刚才的errormsg写入日志了:

# 写入日志
logging.error(traceback.format_exc())

那么我们完善一下整个代码就是这样:

logging.basicConfig(
    filename = './data/error.log', # 日志存储的文件及目录
    format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
    datefmt = '%Y-%m-%d %H:%M:%S'
)

# 写入日志
logging.error(traceback.format_exc())

我们需要在异常出发的时候,将错误写入到日志内。那么需要将这段代码放到except中。可是我们总不能每次都写这么长一段代码,那怎么办呢?嗯,没错,我们需要封装一个函数用于多次调用。

def Myexception():
    # logging的基本配置
    logging.basicConfig(
        filename = './data/error.log', # 日志存储的文件及目录
        format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
        datefmt = '%Y-%m-%d %H:%M:%S'
    )

    # 写入日志
    logging.error(traceback.format_exc())

# 使用自定义异常处理类
try:
    int('bb')
except:
    print('在此处进行异常的处理')
    Myexception() # 在异常处理的代码块中去调用自定义异常类

然后我们将导入库的方法也写进去,这样在我们需要的时候才会进行导入,顺便,我们将这个函数封装成一个类。就便于更多的文件调用:

# 自定义异常日志处理类
class Myexception():
    def __init__(self):
        import traceback
        import logging

        # logging的基本配置
        logging.basicConfig(
            filename='./error.log',# 日志存储的文件及目录
            format='%(asctime)s  %(levelname)s \n %(message)s',# 格式化存储的日志格式
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        # 写入日志
        logging.error(traceback.format_exc())

# 使用自定义异常处理类
try:
    int('bb')
except:
    print('在此处进行异常的处理')
    Myexception() # 在异常处理的代码块中去调用自定义异常类

这样,一个自定义的获取异常之后写入日常的类就定义好了,我们可以在任意地方导入并调用这个类方法,以便获取以及日后查看整个程序中的异常。

附录

标准的异常类

异常名称 描述
BaseException 所有异常的基类
SystemExit 解释器请求退出
KeyboardInterrupt 用户中断执行(通常是输入^C)
Exception 常规错误的基类
StopIteration 迭代器没有更多的值
GeneratorExit 生成器(generator)发生异常来通知退出
StandardError 所有的内建标准异常的基类
ArithmeticError 所有数值计算错误的基类
FloatingPointError 浮点计算错误
OverflowError 数值运算超出最大限制
ZeroDivisionError 除(或取模)零 (所有数据类型)
AssertionError 断言语句失败
AttributeError 对象没有这个属性
EOFError 没有内建输入,到达EOF 标记
EnvironmentError 操作系统错误的基类
IOError 输入/输出操作失败
OSError 操作系统错误
WindowsError 系统调用失败
ImportError 导入模块/对象失败
LookupError 无效数据查询的基类
IndexError 序列中没有此索引(index)
KeyError 映射中没有这个键
MemoryError 内存溢出错误(对于Python 解释器不是致命的)
NameError 未声明/初始化对象 (没有属性)
UnboundLocalError 访问未初始化的本地变量
ReferenceError 弱引用(Weak reference)试图访问已经垃圾回收了的对象
RuntimeError 一般的运行时错误
NotImplementedError 尚未实现的方法
SyntaxError Python 语法错误
IndentationError 缩进错误
TabError Tab 和空格混用
SystemError 一般的解释器系统错误
TypeError 对类型无效的操作
ValueError 传入无效的参数
UnicodeError Unicode 相关的错误
UnicodeDecodeError Unicode 解码时的错误
UnicodeEncodeError Unicode 编码时错误
UnicodeTranslateError Unicode 转换时错误
Warning 警告的基类
DeprecationWarning 关于被弃用的特征的警告
FutureWarning 关于构造将来语义会有改变的警告
OverflowWarning 旧的关于自动提升为长整型(long)的警告
PendingDeprecationWarning 关于特性将会被废弃的警告
RuntimeWarning 可疑的运行时行为(runtime behavior)的警告
SyntaxWarning 可疑的语法的警告
UserWarning 用户代码生成的警告

那么,这节课到这里也就结束了。各位小伙伴,下去以后记得勤加练习。下课。

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

推荐阅读更多精彩内容