第7 章 异常与断言

本文详细介绍了Python中异常的普遍性和处理方式,特别是try-except结构的使用,以及如何通过异常作为控制流手段。作者强调了处理异常的重要性,展示了如何避免未处理异常导致程序崩溃,并通过实例展示了如何使用assert语句进行状态验证和调试。
摘要由CSDN通过智能技术生成

“异常”通常被定义为“不符合规范的东西”,因此有些罕见。但在Python中,异常十分常见,
简直到处都是。实际上,标准Python库中的所有模块都使用异常,Python本身在很多不同的环境
中也会抛出异常。你肯定已经见到过一些异常了。
打开一个Python shell并输入:
test = [1,2,3]
test[3]
解释器会给出如下回应:
IndexError: list index out of range
IndexError是程序试图访问一个位于可索引类型边界之外的元素时,由Python抛出的异常类
型。IndexError后面的字符串提供了一些附加信息,说明了引发异常的原因。
Python语言中内置的多数异常都用来处理这样一种情况,即程序试图使用不恰当的语义结构
执行语句。(在本章后面的内容中,我们也会使用一些特殊的异常——它们不处理程序错误。)那
些努力编写并运行Python程序的读者们(我们希望是所有读者)应该已经遇到了很多异常,最常
见的异常类型是TypeError、IndexError、NameError和ValueError。
7.1 处理异常
直到现在,我们还是将异常当作致命错误进行处理。发生异常时,程序就终止(在这种情况
下,“崩溃”应该是一个更恰当的词语),然后我们回到代码,试图弄清为什么会出错。程序因为
一个异常被抛出而终止时,我们称抛出了一个未处理异常。
异常没有必要导致程序终止。异常在被抛出时,可以也应该由程序进行处理。有时候抛出异
常的原因是程序中有错误(比如访问一个不存在的变量),但很多时候,异常是程序员可以也应
该预料到的事情。程序会试图打开一个不存在的文件。如果交互式程序要求用户输入信息,那么
用户可能会输入一些不合适的内容。
如果知道了一行代码在运行时可能引发异常,那么你应该处理异常。在一个优秀程序中,未
处理异常才是真正的异常。
我们看一下这段代码:

successFailureRatio = numSuccesses/numFailures
print('The success/failure ratio is', successFailureRatio)
print('Now here')

多数情况下,这段代码运行良好,但如果numFailures碰巧是0,那么试图除以0就会使Python运
行时系统抛出一个ZeroDivisionError异常,程序也根本执行不到print语句。
最好按照以下方式改写这段程序:

try:
successFailureRatio = numSuccesses/numFailures
print('The success/failure ratio is', successFailureRatio)
except ZeroDivisionError:
print('No failures, so the success/failure ratio is undefined.')
print('Now here')

进入try代码块时,解释器试图对表达式numSuccesses/numFailures进行求值。如果表达式
求值成功,程序就将这个表达式的值赋给变量successFailureRatio,执行try代码块末尾的
print语句,然后前进到try-except结构后面的print语句。但是,如果表达式求值过程中抛出
ZeroDivisionError异常,控制流就立刻跳到except代码块(跳过try代码块中的赋值语句和print
语句),执行except代码块中的print语句,然后继续执行try-except代码块后面的print语句。
实际练习:实现一个满足以下规范的函数。请使用try-except代码块。

def sumDigits(s):
"""假设s是一个字符串
返回s中十进制数字之和
例如,如果s是'a2b3c',则返回5"""

我们再看另一个例子,考虑以下代码:

val = int(input('Enter an integer: '))
print('The square of the number you entered is', val**2)

如果用户善解人意地输入了一个可以转换为整数的字符串,那么万事大吉。但是,如果用户输入
了abc呢?执行这行代码会使Python运行时系统抛出一个ValueError异常,print语句也根本不
会执行。
程序员应该按照下面的方式写这段代码:

while True:
val = input('Enter an integer: ')
try:
val = int(val)
print('The square of the number you entered is', val**2)
break #跳出while循环
except ValueError:
print(val, 'is not an integer')

进入循环之后,程序会要求用户输入一个整数。一旦用户完成输入,程序就执行try-except
代码块。如果try代码块中的前两行语句都没有引发ValueError异常,那么就执行break语句,
跳出while循环。但是,如果执行try代码块时抛出ValueError异常,控制流就立刻转移到except代码块。因此,如果用户输入了一个不能转换为整数的字符串,程序就会要求用户重新输入。这
样,不论用户输入什么内容,都不会引发一个未处理异常。
这个修改的负面影响是,程序代码从2行变成了8行。如果有很多地方要求用户输入整数,那
就比较成问题了。当然,这个问题可以通过引入一个函数得到解决:

def readInt():
while True:
val = input('Enter an integer: ')
try:
return(int(val)) #返回前将str转换为int
except ValueError:
print(val, 'is not an integer')

更棒的是,这个函数可以扩展为接受任意类型的输入:

def readVal(valType, requestMsg, errorMsg):
while True:
val = input(requestMsg + ' ')
try:
return(valType(val)) #返回前将str转换为valType
except ValueError:
print(val, errorMsg)
readVal(int, 'Enter an integer:', 'is not an integer')

函数readVal是多态的,也就是说,它可以兼容各种类型的参数。这种函数在Python中很容
易编写,因为类型是一等对象。现在我们可以使用以下代码要求用户输入整数:

val = readVal(int, 'Enter an integer:', 'is not an integer')

异常看上去不太友好(毕竟,如果不处理,异常会使程序崩溃),但总好于其他处理方式。
试想,如果要将字符串'abc'转换成一个int类型的对象,类型转换函数int该如何是好?它可以
返回一个对应于该字符串编码的整数,但这样与程序员的初衷相悖。或者,它可以返回一个特殊
值None。这样,程序员还需要插入一些代码检查类型转换是否返回None。如果忘记检查,他就
要承担程序出现一些奇怪错误的风险。
使用异常时,程序员仍然需要编写一些代码处理特定异常。如果忘记处理某个异常,那么这
个异常就被抛出,程序也随之立刻停止。这是件好事,它可以警告用户,让他们知道程序出现了
一些问题。(正如我们在第6章讨论过的,显性错误要远远好于隐性错误。)而且,它还会明确告
知程序调试者哪里发生了错误。
如果一段程序代码中可能引发的异常类型不止一种,那么保留字except后面可以接一个异常
元组,如下所示:

except (ValueError, TypeError):

这种情况下,如果try代码块中引发了任何一种异常,都会进入except代码块。
或者,我们可以为每种异常编写一个单独的except代码块,这样可以使程序根据抛出的异常
选择相应操作。如果代码是这样的:

except:

那么,如果try代码块中抛出任何一种异常,程序都会进入except代码块。图7-1展示了这些特性。

def getRatios(vect1, vect2):
"""假设vect1和vect2是长度相同的数值型列表
返回一个包含vect1[i]/vect2[i]中有意义的值的列表"""
ratios = []
for index in range(len(vect1)):
try:
ratios.append(vect1[index]/vect2[index])
except ZeroDivisionError:
ratios.append(float('nan')) #nan = 不是一个数
except:
raise ValueError('getRatios called with bad arguments')
return ratios

图7-1 使用异常作为控制流

7.2 将异常用作控制流
不要只把异常看作错误,它还是一种方便的控制流机制,可以使程序更加简洁。
在很多编程语言中,处理错误的标准方法是使函数返回一个特定值(与Python中的None很相
似)来表示出现错误。每次函数调用都必须检查返回值是否是这个特定值。在Python中,更常见
的做法是,当函数不能返回一个符合规格说明的结果时,就抛出一个异常。
Python语言中的raise语句可以强制引发一个特定的异常。raise语句的形式如下:

raise exceptionName(arguments)

其中exceptionName通常是一种内置的异常,如ValueError。当然,程序员可以通过为内置的
Exception类创建一个子类,来定义一个新的异常。不同类型的异常可以有不同类型的参数,但
大多数时候参数都是一个字符串,用来描述引发异常的原因。
实际练习:实现一个满足以下规范的函数。

def findAnEven(L):
"""假设L是一个整数列表
返回L中的第一个偶数
如果L中没有偶数,则抛出ValueError异常"""

返回去看一下图7-1中的函数定义。
对应try代码块的有两个except代码块。如果在try代码块中抛出异常,那么Python先检查这
个异常是不是ZeroDivisionError。如果是,就将一个float类型的特殊值nan追加到ratios中。
(nan表示“不是一个数”,没有对应它的字面量,但可以通过将字符串'nan'或'NaN'转换为float
类型来表示。当nan在一个float类型的表达式中用作操作数时,这个表达式的值也是nan。)如
果异常不是ZeroDivisionError,那么代码就执行第二个except代码块,抛出一个带有相应字符串的ValueError异常。
理论上,第二个except代码块永远不会被执行,因为代码调用getRatios时应该遵守函数规
范中的假设。但是,检查是否遵守了这些假设只能增加一些计算负担,没有什么实际意义,所以
还不如使用异常进行防御性编程和检查。
以下代码演示了程序使用getRatios函数的方法。except ValueError as msg:这行代码中
的名称msg绑定了抛出ValueError时使用的参数(一个字符串)。①执行以下代码:

try:
print(getRatios([1.0,2.0,7.0,6.0], [1.0,2.0,0.0,3.0]))
print(getRatios([], []))
print(getRatios([1.0, 2.0], [3.0]))
except ValueError as msg:
print(msg)

会输出:

[1.0, 1.0, nan, 2.0]
[]
getRatios called with bad arguments

图7-2中给出的是对同样规范的另一种实现,没有使用try-except。

def getRatios(vect1, vect2):
"""假设vect1和vect2是长度相同的数值型列表
返回一个包含vect1[i]/vect2[i]中有意义的值的列表"""
ratios = []
if len(vect1) != len(vect2):
raise ValueError('getRatios called with bad arguments')
for index in range(len(vect1)):
vect1Elem = vect1[index]
vect2Elem = vect2[index]
if (type(vect1Elem) not in (int, float))\
or (type(vect2Elem) not in (int, float)):
raise ValueError('getRatios called with bad arguments')
if vect2Elem == 0.0:
ratios.append(float('NaN')) #NaN = 不是一个数
else:
ratios.append(vect1Elem/vect2Elem)
return ratios

图7-2 不使用try-except的控制流

和图7-1中的代码相比,图7-2的代码更长,更难以阅读,效率也更差。(图7-2的代码中,如
果去掉局部变量vect1Elem和vect2Elem,代码会稍短一些,但付出的代价是效率更差,因为要
反复地对列表进行索引。)
我们再看一个例子,如图7-3所示。函数getGrades或者返回一个值,或者抛出一个带有参数值的异常。如果对open函数的调用引发了一个IOError,那么getGrades就抛出一个ValueError
异常。它本可以忽略IOError,让调用getGrades的那部分代码去处理这个异常,但这样会使调
用代码在出现问题时没有足够的信息判断出错位置。调用getGrades的代码或者使用返回值计算
出另一个值,或者处理异常并打印出带有出错原因的错误信息。

def getGrades(fname):
try:
gradesFile = open(fname, 'r') #open file for reading
except IOError:
raise ValueError('getGrades could not open ' + fname)
grades = []
for line in gradesFile:
try:
grades.append(float(line))
except:
raise ValueError('Unable to convert line to float')
return grades
try:
grades = getGrades('quiz1grades.txt')
grades.sort()
median = grades[len(grades)//2]
print ('Median grade is', median)
except ValueError as errorMsg:
print ('Whoops.', errorMsg)

图7-3 计算评分

7.3 断言
Python语言中的assert语句为程序员提供了一种确保程序状态符合预期的简单方法。assert
语句可以有以下两种形式:
assert Boolean expression
或者:
assert Boolean expression, argument
执行assert语句时,先对布尔表达式求值。如果值为True,程序就愉快地继续向下执行;
如果值为False,就抛出一个AssersionError异常。
断言是一种非常有用的防御性编程工具,可以用来确保函数参数具有恰当的类型。它同时也
是一种非常有用的调试工具,可以确保中间值符合预期,或者确保函数返回一个可接受的值。


 

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

___Y1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值