相信大家在跟随教程到现在,已经遇到过 Python 不同数量的报错了。但是遇到报错不需要害怕,因为我们有句话说:能够报错的就不是错,真正的错是不会自己报错的。
今天我们就来讲一下该如何做异常处理,首先需要完成教程:多多教Python:Python 基本功: 6. 第一个完整的程序zhuanlan.zhihu.com多多教Python:Python 基本功: 7. 介绍函数zhuanlan.zhihu.com
并且已经成功运行了教程里介绍的完整的程序。
教程需求:Mac OS (Windows, Linux 会略有不同)
安装了 Python 3.0 版本以上, PyCharm
异常 Exception
当 Python 程序遇到无法执行的命令的时候,就会把无法执行的命令和一堆内存里的数据给反馈出来,这时候程序就停止了,并且留下了一堆代码信息,有点像 Windows 时代的蓝屏。我们都知道用 Windows 最讨厌的就是蓝屏死机,好在 Python 是一门设计巧妙的语言,我们可以通过异常处理的语法来避免程序死机报错,让我们的程序哪怕遇到问题了仍然能正常的运行下去。
现在我们来看一下 Python 中异常处理的语法:
try:
# do something
except Exception as error:
# error handling
finally:
# optional, clean uptry: 这里开始进入抓去异常的范围。
# do something: 运行你写的代码。
except ...: 到这里异常抓取范围结束,在这个范围内如果发生指定的异常事件,就会被“抓住”处理。
Exception as error: 这里指定的异常事件是 Exception 类,抓住了之后给其定义一个变量叫 error, 然后进入异常处理。
# error handling: 这里是抓住了异常事件之后做处理。
finally: 这是可有可无的,不论异常有没有出现被抓住,都会进入这个语法,一般是用来做数据清理,并且保证可以被执行的一段话。
Python的异常处理语法是非常简单的,具体如何使用可能大家还不是很明白,没关系,在之后的实际操作中可以慢慢体会。我们先从比较高层次来了解 Python 异常的种类,来看看 Python 到底可以抛出哪些异常类型:
+-- BaseException (new; broader inheritance for subclasses)
+-- Exception
+-- GeneratorExit (defined in PEP 342 [1])
+-- StandardError
+-- ArithmeticError
+-- DivideByZeroError
+-- FloatingPointError
+-- OverflowError
+-- AssertionError
+-- AttributeError
+-- EnvironmentError
+-- IOError
+-- EOFError
+-- OSError
+-- ImportError
+-- LookupError
+-- IndexError
+-- KeyError
+-- MemoryError
+-- NameError
+-- UnboundLocalError
+-- NotImplementedError (stricter inheritance)
+-- SyntaxError
+-- IndentationError
+-- TabError
+-- TypeError
+-- RuntimeError
+-- UnicodeError
+-- UnicodeDecodeError
+-- UnicodeEncodeError
+-- UnicodeTranslateError
+-- ValueError
+-- ReferenceError
+-- StopIteration
+-- SystemError
+-- Warning
+-- DeprecationWarning
+-- FutureWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+ -- WindowsError
+-- KeyboardInterrupt (stricter inheritance)
+-- SystemExit (stricter inheritance)
这个列表是 Python 全部的异常类型,直接从 Python 3.8 官网上面拷贝下来的,虽然我不是很喜欢直接从别的地方拷贝引用,但是异常的种类还是很需要记忆下来的。其实你一旦熟悉了异常种类,在之后的程序设计,运行中就可以少走很多弯路,避免很多坑。
因为这是一个父子的结构,最高级的 BaseException 是一个虚拟类,我们之后会讲到什么是虚拟类。随后的三大类分别是:Exception: 这一类就是在上面例子用到的,包括了软件里几乎会出现的所有错误:计算错误,环境错误,内存错误,类型错误,运行错误,网络错误等等。
KeyboardInterrupt: 这一类错误是 Python 程序收到非正常操作系统的指令时发出的。情况在 多多教Python:Python 基本功: 1. Hello world 中提到的用 cntrl+Z 来打断退出程序。
SystemExit: 这类错误一般是操作系统直接关闭程序时发出的,情况包括程序里面有恶意代码被操作系统发现,直接关停。
注意,如果你在 except... 语句中特地抓去的是内存错误,那么当发生网络类型的错误的时候,就不会进入错误被抓去的语句中。但是如果你抓取的是这个类型的上一级,父类,则其子类报错的时候都会被抓住。
异常处理 Exception Handling
既然介绍了语法,那么我们来看看如何正确的处理一些异常。现在回到多多教Python:Python 基本功: 7. 介绍函数 里的例子,这里我们的程序可以正常的运行,但是没有任何异常处理代码,如果发生了意外情况程序就会直接关停,那我们来做一个最简单的操作:
if __name__ == '__main__':
# 报告文件名
if len(sys.argv) > 1:
report_date = sys.argv[1]
else:
report_date = ""
abc_file = 'abc_date_price.txt'
try:
abc_dict = report_daily_price(ticker='ABC', input_file=abc_file, report_date=report_date)
except Exception as err:
print(err)
abc_dict = {}
finally:
print(abc_dict)
在 main() 方法里调用函数 report_daily_price 的时候,我们加上了异常处理,抓去 Exception 或者其子类的异常,如果抓取到了则打印出异常,并且设立一个空的字典。最后不论抓取到异常与否,都打印出字典。这里如果函数报错,则函数就不会有回复,所以在抓取到错误的时候要重新建立一个字典,这样可以在 finally... 语句中打印出来。
这么做的好处是如果函数报错,你可以抓住错误并且直接在程序内部处理掉,否则程序就会退出,并且报错的语句也有可能丢失。
接下来我们进入函数,在函数里做一些更加细致的异常处理:
try:
with open(report_file, 'r') as file_to_write:
json.dump(output_dict, file_to_write)
except IOError as io_error:
print("IO error caught: " + str(io_error))
在函数最后把字典以 Json 格式写入文件时候,我们加上异常处理,并且把 open() 里的格式参数从 'w' 改成 'r',在 多多教Python:Python 基本功: 4. 读写文件 已经讲过了,'w' 代表可以写入,‘r’ 代表了只能读取,所以我们这里打开了一个只读文件尝试写入,看看会发生什么情况:
IO error caught: not writable
不出意料,我们抓住了这个异常,是一个 I/O 错误类,I/O 代表了数据的读写通道错误,一般会出现在保存在硬盘的文件读写上。并且错误语句也很明显,“not writable” 就是无法写入的意思。
下面我们再来举一个例子:
try:
ret_pcnt = (ret_dict[datetime(2019, 8, 18)] - ret_dict[datetime(2019, 7, 10)]) / ret_dict[datetime(2019, 7, 18)]
except KeyError as key_error:
print("Key error caught: " + str(key_error))
ret_pcnt = 0
这里在计算回报率的时候,加上了 KeyError, 钥匙错误的抓取。KeyError 在字典查询的时候非常常见,而这里我们故意把一个日期 datetime(2019, 7, 19) 尝改成一个字典中不存在的钥匙 datetime(2019, 8, 19),然后运行。不出所料,我们抓住了这个错误,并且报错是:
Key error caught: datetime.datetime(2019, 8, 18, 0, 0)
KeyError 报错会同时把这个 Key 的数值给报错出来,告诉你是这个 Key 出现了错误,还是很人性化的。
上报错误 Raise Exception
有的时候你知道这个地方有可能会出现异常,但是不想在这个函数里解决这个异常,就交给函数外部处理,甚至你可以主动的扔出一个异常,通过语法 Raise:
try:
ret_pcnt = (ret_dict[datetime(2019, 8, 18)] - ret_dict[datetime(2019, 7, 10)]) / ret_dict[datetime(2019, 7, 18)]
except KeyError as key_error:
print("Key error caught: " + str(key_error))
ret_pcnt = 0
if ret_pcnt == 0:
raise ZeroDivisionError("Return pcnt should not be 0, check calculation")
紧接着第一个 try...except... 语句,我们加上了如果回报率是0,则扔出 ZeroDivisionError, 并且附带一句话请检查计算。这里扔出的 ZeroDivisionError 是指数据处以0后会无限大的错误。这是一个预防性的上报错误,以防之后当遇到除以 回报率的计算的时候出现 ZeroDivisionError。在这里就举一个例子,之后会教你自己根据需求创建一个错误类。
运行程序,我们会发现函数会进入 raise... 语句,扔出 ZeroDivisionError, 然后函数就结束了。随后在函数外部,main() 方法下这个异常被抓住,并且异常的语句,也就是我们写的检查计算语句,被打印出来。
小结:
异常处理对于一个强壮的程序很重要。如果你想写一个 Python 机器人不经常掉线,或者蓝屏的,你需要知道里面每一行代码有可能出现什么错误,并且用正确的错误类型抓住。当然在基础篇我们如果能做到在可能出错的地方用最基本的 Exception 抓取,然后把错误打印出来,就已经很到位了。接下来加上两个链接有兴趣的小伙伴可以去看看:
一个外部的异常处理教程:Python 异常处理 | 菜鸟教程www.runoob.com
Python 3.8 官方异常结构:PEP 348 -- Exception Reorganization for Python 3.0www.python.org