Python入门:Python内置的异常机制

本章剩余部分将会专门介绍Python内置的异常机制。整个Python异常机制都是按照面向对象的规范搭建的,这使得它灵活而又兼具扩展性。即便大家对面向对象编程(object-oriented programming,OOP)不太熟悉,使用异常时也无须特地去学习面向对象技术。

异常是Python函数用raise语句自动生成的对象。在异常对象生成后,引发异常的raise语句将改变Python程序的执行方式,这与正常的执行流程不同了。不是继续执行raise的下一条语句,也不执行生成异常后的下一条语句,而是检索当前函数调用链,查找能够处理当前异常的处理程序。如果找到了异常处理程序,则会调用它,并访问异常对象获取更多信息。如果找不到合适的异常处理程序,程序将会中止并报错。

{请求容易认可难!}

总体而言,Python对待错误处理的方式与Java等语言的常见方式不同。那些语言有赖于在错误发生之前就尽可能地检查出来,因为在错误发生后再来处理异常,往往要付出各种高昂的成本。本章第一节已对这种方式有所介绍,有时也被称为“三思而后行”(Look Before You Leap, LBYL)方式。

而Python可能更依赖于“异常”,在错误发生之后再做处理。虽然这种依赖看起来可能会有风险,但如果“异常”能使用得当,代码就会更加轻巧,可读性也更好,只有在发生错误时才会进行处理。这种Python式的错误处理方法通常被称为“先斩后奏”(Easier to Ask Forgiveness than Permission, EAFP)。

14.2.1 Python异常的类型

为了能够正确反映引发错误的实际原因,或者需要报告的异常情况,可以生成各种不同类型的异常对象。Python 3.6提供的异常对象有很多类型:

 
  1. BaseException
  2. SystemExit
  3. KeyboardInterrupt
  4. GeneratorExit
  5. Exception
  6. StopIteration
  7. ArithmeticError
  8. FloatingPointError
  9. OverflowError
  10. ZeroDivisionError
  11. AssertionError
  12. AttributeError
  13. BufferError
  14. EOFError
  15. ImportError
  16. ModuleNotFoundError
  17. LookupError
  18. IndexError
  19. KeyError
  20. MemoryError
  21. NameError
  22. UnboundLocalError
  23. OSError
  24. BlockingIOError
  25. ChildProcessError
  26. ConnectionError
  27. BrokenPipeError
  28. ConnectionAbortedError
  29. ConnectionRefusedError
  30. ConnectionResetError
  31. FileExistsError
  32. FileNotFoundError
  33. InterruptedError
  34. IsADirectoryError
  35. NotADirectoryError
  36. PermissionError
  37. ProcessLookupError
  38. TimeoutError
  39. ReferenceError
  40. RuntimeError
  41. NotImplementedError
  42. RecursionError
  43. SyntaxError
  44. IndentationError
  45. TabError
  46. SystemError
  47. TypeError
  48. ValueError
  49. UnicodeError
  50. UnicodeDecodeError
  51. UnicodeEncodeError
  52. UnicodeTranslateError
  53. Warning
  54. DeprecationWarning
  55. PendingDeprecationWarning
  56. RuntimeWarning
  57. SyntaxWarning
  58. UserWarning
  59. FutureWarning
  60. ImportWarning
  61. UnicodeWarning
  62. BytesWarningException
  63. ResourceWarning

Python的异常对象是按层级构建的,上述异常列表中的缩进关系正说明了这一点。正如第10章中所示,可以从__builtins__模块中获取按字母顺序排列的异常对象清单。

每种异常都是一种Python类,继承自父异常类。但大家如果还未接触过OOP,也不必担心。例如,IndexError也是LookupError类和Exception类(通过继承),且还是BaseException

这种层次结构是有意为之的,大部分异常都继承自Exception,强烈建议所有的用户自定义异常也都应是Exception的子类,而不要是BaseException的子类。理由如下。

 
  1. try:
  2. # 执行一些异常操作
  3. except Exception:
  4. # 处理异常

{:—}上述代码中,仍旧可以用Ctrl+C中止try语句块的执行,且不会引发异常处理代码。因为KeyboardInterrupt异常不是Exception的子类。

虽然在文档中可以找到每种异常的解释,但最常见的几种异常通过动手编程就能很快熟悉了。

14.2.2 引发异常

异常可由很多Python内置函数引发:

 
  1. >>> alist = [1, 2, 3]
  2. >>> element = alist[7]
  3. Traceback (innermost last):
  4. File "<stdin>", line 1, in ?
  5. IndexError: list index out of range

Python内置的错误检查代码,将会检测到第二行请求读取的列表索引值不存在,并引发IndexError异常。该异常一直传回顶层,也就是交互式Python解释器,解释器对其处理的方式是打印出一条消息表明发生了异常。

在自己的代码中,还可以用raise语句显式地引发异常。raise语句最基本的形式如下:

 
  1. raise exception(args)

exception(args)部分会创建一个异常对象。新异常对象的参数通常应是有助于确定错误情况的值,后续将会介绍。在异常对象被创建之后,raise会将其沿着Python函数堆栈向上层抛出,也就是当前执行到raise语句的函数。新创建的异常将被抛给堆栈中最近的类型匹配的异常捕获代码块。如果直到程序顶层都没有找到相应的异常捕获代码块,程序就会停止运行并报错,在交互式会话中则会把错误消息打印到控制台。

请尝试以下代码:

 
  1. >>> raise IndexError("Just kidding")
  2. Traceback (innermost last):
  3. File "<stdin>", line 1, in ?
  4. IndexError: Just kidding

上面用raise生成的消息,乍一看好像与之前所有的Python列表索引错误消息都很类似。再仔细查看一下就会发现,情况并非如此。实际的错误并不像之前的错误那么严重。

创建异常时,常常会用到字符串参数。如果给出了第一个参数,大部分内置Python异常都会认为该参数是要显示出来的信息,作为对已发生事件的解释。不过情况并非总是如此,因为每个异常类型都有自己的类,创建该类的异常时所需的参数,完全由类的定义决定。此外,由程序员创建的自定义异常,经常用作错误处理之外的用途,因此可能并不会用文本信息作为参数。

14.2.3 捕获并处理异常

异常机制的重点,并不是要让程序带着错误消息中止运行。要在程序中实现中止功能,从来都不是什么难事。异常机制的特别之处在于,不一定会让程序停止运行。通过定义合适的异常处理代码,就可以保证常见的异常情况不会让程序运行失败。或许可以通过向用户显示错误消息或其他方法,或许还可能把问题解决掉,但是不会让程序崩溃。

以下演示了Python异常捕获和处理的基本语法,用到了tryexcept,有时候还会用else关键字:

 
  1. try:
  2. body
  3. except exception_type1 as var1:
  4. exception_code1
  5. except exception_type2 as var2:
  6. exception_code2
  7. .
  8. .
  9. .
  10. except:
  11. default_exception_code
  12. else:
  13. else_body
  14. finally:
  15. finally_body

首先执行的是try语句的body部分。如果执行成功,也就是try语句没有捕获到有异常抛出,那就执行else_body部分,并且try语句执行完毕。因为这里有条finally语句,所以接着会执行finally_body部分。如果有异常向try抛出,则会依次搜索各条except子句,查找关联的异常类型与抛出的异常匹配的子句。如果找到匹配的except子句,则将抛出的异常赋给变量,变量名在关联异常类型后面给出,并执行匹配except子句内的异常处理代码。例如,except exception_type as var:这行匹配上了某抛出的异常exc,就会创建变量var,并在执行该except语句的异常处理代码之前,将var的值赋为excvar不是必需的,可以只出现except exception_type:这种写法,给定类型的异常仍然能被捕获,只是不会把异常赋给某个变量了。

如果没有找到匹配的except子句,则该try语句就无法处理抛出的异常,异常会继续向函数调用链的上一层抛出,期望有外层的try能够处理。

try语句中的最后一条except子句,可以完全不指定任何异常类型,这样就会处理所有类型的异常。对于某些调试工作和非常快速的原型开发,这种技术可能很方便。但通常这不是个好做法,所有错误都被except子句掩盖起来了,可能会让程序的某些行为令人难以理解。

try语句的else子句是可选的,也很少被用到。当且仅当try语句的body部分执行时没有抛出任何错误时,else子句才会被执行。

try语句的finally子句也是可选的,在tryexceptelse部分都执行完毕后执行。如果try块中有异常引发并且没被任何except块处理过,那么finally块执行完毕后会再次引发该异常。因为finally块始终会被执行,所以能在异常处理完成后,通过关闭文件、重置变量之类的操作提供一个加入资源清理代码的机会。

动手题:捕获异常 编写代码读取用户输入的两个数字,将第一个数字除以第二个数字。检查并捕获第二个数字为0时的异常(ZeroDivisionError)。

14.2.4 自定义新的异常

定义自己的异常十分简单。用以下两行代码就能搞定:

 
  1. class MyError(Exception):
  2. pass

{:—}上述代码创建了一个类,该类将继承基类Exception中的所有内容。不过如果不想弄清楚细节,则大可不必理会。

以上异常可以像其他任何异常一样引发、捕获和处理。如果给出一个参数,并且未经捕获和处理,参数值就会在跟踪信息的最后被打印出来:

 
  1. >>> raise MyError("Some information about what went wrong")
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. __main__.MyError: Some information about what went wrong

当然,上述参数在自己编写的异常处理代码中也是可以访问到的:

 
  1. try:
  2. raise MyError("Some information about what went wrong")
  3. except MyError as error:
  4. print("Situation:", error)

运行结果将如下所示:

 
  1. Situation: Some information about what went wrong

如果引发异常时带有多个参数,这些参数将会以元组的形式传入异常处理代码中,元组通过error变量的args属性即可访问到:

 
  1. try:
  2. raise MyError("Some information", "my_filename", 3)
  3. except MyError as error:
  4. print("Situation: {0} with file {1}\n error code: {2}".format(
  5. error.args[0],
  6. error.args[1], error.args[2]))

运行结果将如下所示:

 
  1. Situation: Some information with file my_filename
  2. error code: 3

异常类型是常规的Python类,并且继承自Exception类,所以建立自己的异常类型层次架构,供自己的代码使用,就是一件比较简单的事情。第一次阅读本书时,不必关心这一过程。读完第15章之后,可以随时回来看看。如何创建自己的异常,完全由需求决定。如果正在编写的是个小型程序,可能只会生成一些唯一的错误或异常,那么如上所述采用Exception类的子类即可。如果正在编写大型的、多文件的、完成特定功能的代码库(如天气预报库),那就可以考虑单独定义一个名为WeatherLibraryException的类,然后将库中所有的不同异常都定义为WeatherLibraryException的子类。

速测题:“异常”类 假设MyError继承自Exception类,请问except Exception as eexcept MyError as e有什么区别?

14.2.5 用assert语句调试程序

assert语句是raise语句的特殊形式:

 
  1. assert expression, argument

如果expression的结算结果为False,同时系统变量__debug__也为True,则会引发携带可选参数argumentAssertionError异常。__debug__变量默认为True。带-O-OO参数启动Python解释器,或将系统变量PYTHONOPTIMIZE设为True,则可以将__debug__置为False。可选参数argument可用于放置对该assert的解释信息。

如果__debug__False,则代码生成器不会为assert语句创建代码。在开发阶段,可以用assert语句配合调试语句对代码进行检测。assert语句可以留存在代码中以备将来使用,在正常使用时不存在运行开销:

 
  1. >>> x = (1, 2, 3)
  2. >>> assert len(x) > 5, "len(x) not > 5"
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. AssertionError: len(x) not > 5

动手题:assert语句 请编写一个简单的程序,让用户输入一个数字,利用assert语句在数字为0时引发异常。首先请测试以确保assert语句的执行,然后通过本小节提到的方法禁用assert

14.2.6 异常的继承架构

之前已经介绍过,Python的异常是分层的架构。本节将对这种架构作深入介绍,包括这种架构对于except子句如何捕获异常的意义。

请看以下代码:

 
  1. try:
  2. body
  3. except LookupError as error:
  4. exception code
  5. except IndexError as error:
  6. exception code

这里将会捕获IndexErrorLookupError这两种异常。正巧IndexErrorLookupError的子类。如果body抛出IndexError,那么错误会首先被“except LookupError as error:”这行检测到。由于IndexError继承自LookupError,因此第一条except子句会成功执行,第二条except子句永远不会用到,因为它的运行条件被第一条except子句包含在内了。

相反,将两条except子句的顺序互换一下,可能就有意义了。这样第一条子句将处理IndexError,第二条子句将处理除IndexError之外的LookupError

14.2.7 示例:用Python编写的磁盘写入程序

本节将重新回到字处理程序的例子,在把文档写入磁盘时,该程序需要检查磁盘空间不足的情况:

 
  1. def save_to_file(filename) :
  2. try:
  3. save_text_to_file(filename)
  4. save_formats_to_file(filename)
  5. save_prefs_to_file(filename)
  6. .
  7. .
  8. .
  9. except IOError:
  10. ...处理错误...
  11. def save_text_to_file(filename):
  12. ...调用底层函数来写入文本大小...
  13. ...调用底层函数来写入实际的文本数据...
  14. .
  15. .
  16. .

注意,错误处理代码很不显眼,在save_to_file函数中与一系列磁盘写入调用放在一起了。那些磁盘写入子函数都不需要包含任何错误处理代码。程序一开始会比较容易开发,以后要添加错误处理代码也很简单。程序员经常这么干,尽管这种实现顺序不算最理想。

还有一点也值得注意,上述代码并不是只会对磁盘满的错误做出响应,而是会响应所有IOError异常。Python的内置函数无论何时无法完成I/O请求,不管什么原因都会自动引发IOError异常。可能这么做能满足需求,但如果要单独识别磁盘已满的情况,就得再做一些操作。可以在except语句体中检查磁盘还有多少可用空间。如果磁盘空间不足,显然是发生了磁盘满的问题,应该在except语句体内进行处理。如果不是磁盘空间问题,那么except语句体中的代码可以向调用链的上层抛出该IOError,以便交由其他的except语句体去处理。如果这种方案还不足以解决问题,那么还可以进行一些更为极端的处理,例如,找到Python磁盘写入函数的C源代码,并根据需要引发自定义的DiskFull异常。最后的这种方案并不推荐,但在必要时应该知道有这种可能性的存在,这是很有意义的。

14.2.8 示例:正常计算过程中的异常

异常最常见的用途就是处理错误,但在某些应被视作正常计算过程的场合,也会非常有用。设想一下电子表格程序之类的实现时可能会遇到的问题。像大多数电子表格一样,程序必须能实现涉及多个单元格的算术运算,并且还得允许单元格中包含非数字值。在这种应用程序中,进行数值计算时碰到的空白单元格,其内容可能被视作0值。包含任何其他非数字字符串的单元格可能被视作无效,并表示为Python的None值。任何涉及无效值的计算,都应返回无效值。

下面首先编写一个函数,用于对电子表格单元格中的字符串进行求值,并返回合适的值:

 
  1. def cell_value(string):
  2. try:
  3. return float(string)
  4. except ValueError:
  5. if string == "":
  6. return 0
  7. else:
  8. return None

Python的异常处理能力使这个函数写起来十分简单。在try块中,将单元格中的字符串用内置float函数转换为数字,并返回结果。如果参数字符串无法转换为数字,float函数会引发ValueError异常。然后异常处理代码将捕获该异常并返回0None,具体取决于参数字符串是否为空串。

有时候在求值时可能必须要对None值做出处理,下一步就来解决这个问题。在不带异常机制的编程语言中,常规方案就是定义一组自定义的算术求值函数,自行检查参数是否为None,然后用这些自定义函数取代内置函数,执行所有电子表格计算。但是,这个过程会非常耗时且容易出错。而且实际上这是在电子表格程序中自建了一个解释器,所以会导致运行速度的降低。本项目采用的是另一种方案。所有电子表格公式实际上都可以是Python函数,函数的参数是被求值单元格的x、y坐标和电子表格本身,用标准的Python算术操作符计算结果,用cell_value从电子表格中提取必要的值。可以定义一个名为safe_apply的函数,在try块中用相应参数完成公式的调用,根据公式是否计算成功,返回其计算结果或者返回None

 
  1. def safe_apply(function, x, y, spreadsheet):
  2. try:
  3. return function(x, y, spreadsheet)
  4. except TypeError:
  5. return None

上述两步改动,足以在电子表格的语义中加入空值(None)的概念。如果不用异常机制来开发上述功能,那将会是一次很有教益的练习(言下之意是,能体会到相当大的工作量)。

14.2.9 异常的适用场合

使用异常来处理几乎所有的错误,是很自然的解决方案。往往是在程序的其余部分基本完成时,错误处理部分才会被加入进来。很遗憾事实就是如此,不过异常机制特别擅长用易于理解的方式编写这种事后错误处理的代码,更好听的说法是事后多加点错误处理代码。

如果程序中有计算分支已明显难以为继,然后可能有大量的处理流程要被舍弃,这时异常机制也会非常有用。电子表格示例就是这种情况,其他应用场景还有分支限界(branch-and-bound)算法和语法解析(parsing)算法。

速测题:异常 Python异常会让程序强行中止吗?

假定要访问字典对象x,如果键不存在,也就是引发KeyError,则返回None。该如何编写代码达到此目标呢?

动手题:异常 编写代码创建自定义的ValueTooLarge,并在变量x大于1000时引发。

本文截选自《Python 快速入门(第3版)》

这是一本Python快速入门书,基于Python 3.6编写。本书分为4部分,第一部分讲解Python的基础知识,对Python进行概要的介绍;第二部分介绍Python编程的重点,涉及列表、元组、集合、字符串、字典、流程控制、函数、模块和作用域、文件系统、异常等内容;第三部分阐释Python的高级特性,涉及类和面向对象、正则表达式、数据类型即对象、包、Python库等内容;第四部分关注数据处理,涉及数据文件的处理、网络数据、数据的保存和数据探索,最后给出了相关的案例。

本书框架结构清晰,内容编排合理,讲解循序渐进,并结合大量示例和习题,让读者可以快速学习和掌握Python,既适合Python初学者学习,也适合作为专业程序员的简明Python参考书。

猜你喜欢

转载自blog.csdn.net/epubit17/article/details/108506470