13 - 异常处理程序不可能总是一帆风顺。文件可能不存在、网络可能断了、用户可能输入了奇怪的东西。异常处理就是教你的程序怎么优雅地应对这些意外。什么是异常你之前肯定已经见过报错了比如print(1/0)# ZeroDivisionError: division by zeroprint(my_var)# NameError: name my_var is not definedint(abc)# ValueError: invalid literal for int()这些报错在 Python 里叫异常Exception。程序遇到异常就会停下来如果不处理就直接崩了。try-except最基本的异常处理try:result10/0exceptZeroDivisionError:print(不能除以零)try里面放可能出错的代码except里面放出错后怎么应对。如果没出错except就跳过。捕获特定异常try:numint(input(输入一个数字))result100/numprint(f结果{result})exceptValueError:print(请输入有效的数字)exceptZeroDivisionError:print(不能输入 0)可以有多个except分别处理不同的异常。跟if-elif一样匹配到第一个就执行后面的跳过。获取异常信息try:result10/0exceptZeroDivisionErrorase:print(f出错了{e})# 出错了division by zeroprint(f异常类型{type(e)})as e把异常对象存到变量e里你可以查看具体信息。捕获所有异常try:# 一些可能出错的操作risky_operation()exceptExceptionase:print(f出错了{e})Exception是大多数异常的基类能捕获除了KeyboardInterruptCtrlC和SystemExit之外的所有异常。不建议一开始就用except Exception因为这样你什么异常都吞了包括你没想到会发生的。最好是先捕获具体的异常实在兜不住了再用Exception做最后一道防线。try:process_data()exceptFileNotFoundError:print(文件不存在)exceptValueError:print(数据格式错误)exceptExceptionase:print(f未知错误{e})# 最后的兜底try-except-else-finally完整形态try:fopen(data.txt,r)contentf.read()exceptFileNotFoundError:print(文件不存在)else:# try 成功没异常时执行print(f读取了{len(content)}个字符)finally:# 不管有没有异常都会执行print(操作结束)elsetry 没出错才执行。放在else里的好处是如果else里的代码出了异常不会被上面的except意外捕获。finally无论如何都执行。通常用来清理资源关文件、关数据库连接等。不过说实话else用得不多。finally如果你用了with语句也不需要了with 自动处理清理工作。抛出异常用raise主动抛出异常defset_age(age):ifage0:raiseValueError(年龄不能为负数)ifage150:raiseValueError(年龄不太对吧)returnage set_age(-5)# ValueError: 年龄不能为负数也可以把捕获的异常重新抛出去try:process_data()exceptValueErrorase:print(f处理数据失败{e})raise# 重新抛出让上层处理自定义异常你可以创建自己的异常类型classInsufficientBalanceError(Exception):余额不足异常def__init__(self,balance,amount):self.balancebalance self.amountamountsuper().__init__(f余额不足当前{balance}需要{amount})classBankAccount:def__init__(self,balance0):self.balancebalancedefwithdraw(self,amount):ifamountself.balance:raiseInsufficientBalanceError(self.balance,amount)self.balance-amount# 使用accountBankAccount(100)try:account.withdraw(200)exceptInsufficientBalanceErrorase:print(f取款失败{e})print(f差额{e.amount-e.balance})自定义异常的好处调用者可以精确地捕获你的业务异常而不会跟 Python 内置的异常搞混。常见的异常类型异常什么情况ValueError值不对如int(abc)TypeError类型不对如a 1KeyError字典的键不存在IndexError列表索引越界FileNotFoundError文件不存在AttributeError对象没有这个属性/方法ImportError模块导入失败ZeroDivisionError除以零NameError变量名不存在PermissionError没有权限你不用背这些遇到了查一下就行。写多了自然就记住了。异常处理的几个原则不要吞掉异常# 很糟糕的做法try:important_operation()exceptException:pass# 什么也不做异常被默默吞掉了这是最让人抓狂的代码之一。出了问题你完全不知道发生了什么因为异常被静默忽略了。至少打印个日志importloggingtry:important_operation()exceptExceptionase:logging.error(f操作失败{e})raise# 或者重新抛出精确捕获# 不太好——什么都捕获了try:datajson.loads(text)exceptException:data{}# 更好——只捕获 JSON 解析错误try:datajson.loads(text)exceptjson.JSONDecodeError:data{}捕获范围太广的话可能会掩盖你没预料到的 bug。EAFP vs LBYLPython 社区有个哲学叫EAFPEasier to Ask Forgiveness than Permission——先干再说出错了再处理。# EAFP 风格推荐try:valuemy_dict[key]exceptKeyError:value默认值# LBYL 风格Look Before You Leapifkeyinmy_dict:valuemy_dict[key]else:value默认值Python 更推崇 EAFP 风格。当然如果已经有更好的 API比如dict.get()那就用它# 最佳valuemy_dict.get(key,默认值)异常链Python 3 支持异常链可以在捕获一个异常时抛出另一个异常同时保留原始信息classConfigError(Exception):passtry:withopen(config.json)asf:configjson.load(f)exceptFileNotFoundErrorase:raiseConfigError(配置文件缺失)fromeexceptjson.JSONDecodeErrorase:raiseConfigError(配置文件格式错误)frome这样报错信息里会同时显示原始异常和新的异常方便追踪问题。一个综合例子importjsonimportloggingfrompathlibimportPath logging.basicConfig(levellogging.INFO)loggerlogging.getLogger(__name__)defload_user_data(filepath:str)-dict:加载用户数据处理各种可能的异常pPath(filepath)try:contentp.read_text(encodingutf-8)exceptFileNotFoundError:logger.warning(f文件不存在{filepath}返回空数据)return{}exceptPermissionError:logger.error(f没有权限读取{filepath})raisetry:datajson.loads(content)exceptjson.JSONDecodeErrorase:logger.error(fJSON 格式错误{e})return{}ifnotisinstance(data,dict):logger.warning(数据格式不对应该是字典)return{}logger.info(f成功加载数据共{len(data)}个字段)returndata这个例子展示了分别处理不同的异常有的异常返回默认值文件不存在、格式错误有的异常向上抛权限错误这种应该让调用者知道用日志记录而不是 printassert 语句assert是一种断言——你确信某个条件一定为真如果不是说明代码有 bug。defcalculate_discount(price,discount):assert0discount1,f折扣必须在 0-1 之间收到{discount}returnprice*(1-discount)print(calculate_discount(100,0.8))# 20.0# calculate_discount(100, 1.5) # AssertionError: 折扣必须在 0-1 之间收到1.5assert等价于ifnotcondition:raiseAssertionError(message)assert 的使用场景适合检查不应该发生的情况帮助开发阶段发现 bug。defget_status_text(code):mapping{200:OK,404:Not Found,500:Error}assertcodeinmapping,f未知状态码{code}returnmapping[code]不适合检查用户输入或运行时可能出现的正常错误。# 错误用法不要用 assert 做输入校验deflogin(username,password):assertusername!,用户名不能为空# 不好用 if raise为什么因为 Python 可以用-O参数运行这时候所有 assert 都会被跳过。如果你的程序逻辑依赖 assert优化模式下就全废了。在测试中常用assert 在单元测试里用得最多后面第 20 章会简单讲 pytestdeftest_add():assertadd(1,2)3assertadd(-1,1)0assertadd(0,0)0warnings 模块有时候你想提醒用户这样用不太好但又不想直接报错中断程序。这时候用warningsimportwarningsdefold_function():warnings.warn(这个函数将在 v2.0 移除请用 new_function(),DeprecationWarning)returnresultresultold_function()# 程序不会中断但会显示警告常用警告类型类型含义UserWarning普通警告默认DeprecationWarning即将废弃的功能FutureWarning未来版本会改变的行为RuntimeWarning运行时可疑行为过滤警告有些警告你不想看到比如第三方库发出来的可以过滤掉importwarnings# 忽略所有警告warnings.filterwarnings(ignore)# 只忽略特定类型的警告warnings.filterwarnings(ignore,categoryDeprecationWarning)# 把警告变成错误测试时有用确保没有隐藏问题warnings.filterwarnings(error)实际开发中warnings主要用于库的开发者通知用户某个功能要废弃了。普通业务代码里用得不多但知道有这个东西看到别人的代码里用就不会懵。本章小结try-except捕获异常finally确保清理代码执行尽量捕获具体异常别一上来就except Exceptionraise主动抛出异常可以自定义异常类不要吞掉异常至少要记日志Python 推崇 EAFP 风格先做再处理异常raise ... from e可以建立异常链assert用于断言不应该发生的情况不要用来做输入校验warnings模块发出非致命警告适合通知用户功能即将废弃面试题Q1try-except-else-finally各自的执行条件是什么点击查看答案try始终执行excepttry 中发生对应异常时执行elsetry 中没有发生异常时执行finally无论如何都执行有异常没异常都执行执行顺序try → (except 或 else) → finallyfinally即使在 try/except 中有 return 语句也会执行return 会等 finally 执行完再返回。Q2raise和raise e有什么区别点击查看答案在 except 块中raise不带参数重新抛出当前异常保留完整的堆栈追踪raise e抛出异常对象 e堆栈追踪会从这一行重新开始try:1/0exceptZeroDivisionErrorase:raise# 堆栈指向 1/0 那一行推荐# raise e # 堆栈指向 raise e 这一行丢失原始位置所以重新抛出时推荐用raise而不是raise e这样更容易定位问题的真正来源。Q3什么是 EAFP跟 LBYL 有什么区别点击查看答案EAFPEasier to Ask Forgiveness than Permission先执行操作出异常了再处理。Python 推荐的风格。LBYLLook Before You Leap先检查条件确认安全了再执行。# EAFPtry:valued[key]exceptKeyError:valuedefault# LBYLifkeyind:valued[key]else:valuedefaultEAFP 的优点代码更简洁避免了检查-执行之间的竞态条件。缺点异常频繁发生时性能较差异常处理有开销。Q4为什么要自定义异常直接 raise ValueError 不行吗点击查看答案自定义异常的好处语义清晰InsufficientBalanceError比ValueError更准确地描述了问题精确捕获调用者可以只捕获你的业务异常不会误捕其他 ValueError携带上下文自定义异常可以包含业务相关的数据如余额、差额异常层次可以建立项目特有的异常层次结构try:account.withdraw(200)exceptInsufficientBalanceError:# 只处理余额不足exceptValueError:# 处理其他值错误金额不能为负等小项目用内置异常也够了大项目建议定义自己的异常体系。