查看: 253|回复: 3

Python异常处理try except完整结构与自定义异常

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
Python 里的运行时错误最终都会变成一个异常对象被抛出,如果调用栈上没有 except 接住,解释器就会打印 traceback 并终止进程。本文按“异常对象—捕获粒度—else/finally 清理—raise 与异常链—自定义异常—断言—EAFP 风格—配置读取实战”的顺序,把 try/except 这套结构里真正影响代码行为的细节整理清楚。

一、异常对象与常见异常类型

最直观的例子:
  1. print(10 / 0)
  2. # ZeroDivisionError: division by zero
复制代码

不处理就会直接崩。按触发场景记住下面这些内置异常,写 except 时才不会乱抓:

- SyntaxError、IndentationError:语法和缩进问题,代码根本跑不起来,属于解释阶段而非运行时。
- NameError:用了未定义的变量名。
- TypeError:类型不匹配,例如 "a" + 1。
- ValueError:类型对但值不对,例如 int("abc")。
- IndexError、KeyError:列表下标越界、字典键不存在。
- AttributeError:对象上没有该属性。
- FileNotFoundError:路径指向的文件不存在。
- ZeroDivisionError:除数为 0。
- ImportError:模块导入失败。

二、try/except 基本结构与捕获粒度

把用户输入、类型转换和除法放进同一个 try,用不同 except 分支区分故障原因:
  1. try:
  2.     num = int(input("请输入数字:"))
  3.     print(10 / num)
  4. except ValueError:
  5.     print("输入的不是数字!")
  6. except ZeroDivisionError:
  7.     print("不能输入 0!")
复制代码

这样用户乱输入时程序只会走对应分支,不会整体崩掉。需要同时按同一逻辑处理多类异常时,可以把异常类型写成元组,并用 as e 绑定异常对象:
  1. try:
  2.     ...
  3. except (ValueError, TypeError) as e:
  4.     print(f"输入有误:{e}")
复制代码

as e 拿到的是异常实例,type(e) 是具体的异常类,str(e) 是原始错误消息,例如 int("abc") 会得到 invalid literal for int() with base 10: 'abc',把它写进日志比只打一句“出错了”有用得多。

要“兜底”可以写 except Exception as e,Exception 是所有常规异常的父类,但 KeyboardInterrupt 和 SystemExit 并不继承它。裸 except:(不写类型)会把这两个也一起吞掉,后果是 Ctrl+C 都停不掉程序,属于明确不该写的写法。

三、else 与 finally 的执行时机

try 只负责可能出现异常的语句,else 在 try 未抛异常时执行,finally 则无论成功失败都执行,典型用途是清理资源:
  1. try:
  2.     f = open("data.txt", encoding="utf-8")
  3.     content = f.read()
  4. except FileNotFoundError:
  5.     print("文件不存在")
  6. else:
  7.     print(f"读取成功,{len(content)} 个字符")
  8. finally:
  9.     print("收尾工作")
复制代码

把业务成功后的逻辑放进 else,可以避免它被本段的 except 误捕获。finally 常用于关闭文件、断开数据库连接、释放锁:
  1. f = None
  2. try:
  3.     f = open("data.txt")
  4.     data = f.read()
  5. except FileNotFoundError:
  6.     data = ""
  7. finally:
  8.     if f:
  9.         f.close()
复制代码

当然这种场景用 with 上下文管理器更省事,finally 更适合 with 管不到的资源。

这里有一个必须避开的坑:在 finally 里 return 会丢弃正在传播的异常。
  1. def bad():
  2.     try:
  3.         raise ValueError("出错了")
  4.     finally:
  5.         return "正常"
  6. print(bad())  # 输出"正常",异常消失
复制代码

四、raise:主动抛出与异常链

参数校验类函数适合在入口处主动 raise,把不合法的调用尽早挡掉:
  1. def set_age(age):
  2.     if not isinstance(age, int):
  3.         raise TypeError("年龄必须是整数")
  4.     if age < 0 or age > 150:
  5.         raise ValueError(f"年龄 {age} 不合法")
  6.     return age
  7. set_age(-5)
  8. # ValueError: 年龄 -5 不合法
复制代码

如果当前层只做记录、希望上层继续处理,可以用裸 raise 原样重抛,不要写成 raise e,裸 raise 保留的是原始 traceback:
  1. try:
  2.     do_something()
  3. except Exception as e:
  4.     print(f"记录日志:{e}")
  5.     raise
复制代码

当异常需要换一种语义再往上抛时,用 from 显式建立异常链,保留根因便于排查:
  1. try:
  2.     int(config["port"])
  3. except KeyError as e:
  4.     raise ValueError("配置文件缺少 port 项") from e
复制代码

五、自定义异常类

业务异常只需要继承 Exception;需要携带上下文数据时,在 __init__ 里先保存属性,再调用父类构造设置消息:
  1. class ValidationError(Exception):
  2.     """数据校验失败的异常"""
  3.     pass
  4. class InsufficientBalanceError(Exception):
  5.     def __init__(self, balance, amount):
  6.         self.balance = balance
  7.         self.amount = amount
  8.         super().__init__(f"余额不足:当前 {balance},需要 {amount}")
  9. def withdraw(balance, amount):
  10.     if amount > balance:
  11.         raise InsufficientBalanceError(balance, amount)
  12.     return balance - amount
  13. try:
  14.     withdraw(100, 200)
  15. except InsufficientBalanceError as e:
  16.     print(e)
  17.     print(f"还差 {e.amount - e.balance} 元")
复制代码

注意除消息之外还把 balance、amount 存成了实例属性,捕获方才能算出差额这类附加信息。相比出错时返回 None,抛自定义异常能让调用方明确区分“失败了”和“返回了一个合法但为空的值”。

六、assert 的正确使用范围
  1. def divide(a, b):
  2.     assert b != 0, "除数不能为 0"
  3.     return a / b
复制代码

assert 条件, 提示信息 在条件为假时抛 AssertionError。它用来检查“绝对不该发生”的内部不变量,不要用它处理用户输入,因为用 python -O 运行时所有断言会被跳过,校验会静默失效。

七、EAFP 与 LBYL

Python 更推崇 EAFP(先做,出错再处理),而不是 LBYL(先检查再做):
  1. # LBYL
  2. if "key" in d:
  3.     value = d["key"]
  4. else:
  5.     value = default
  6. # EAFP
  7. try:
  8.     value = d["key"]
  9. except KeyError:
  10.     value = default
  11. # 更简洁的写法
  12. value = d.get("key", default)
复制代码

EAFP 代码更短,也避免了“检查”和“使用”之间状态被改掉的竞态。但能直接用 get()、setdefault() 这类安全方法解决的场景,优先用方法,不必套 try。

八、实战:带回退的配置读取

配置文件读取是异常处理最典型的落地场景:文件可能不存在,内容可能不是合法 JSON,返回的字典还可能缺字段。
  1. import json
  2. from pathlib import Path
  3. DEFAULT_CONFIG = {
  4.     "theme": "light",
  5.     "language": "zh",
  6.     "max_items": 20
  7. }
  8. def load_config(path="config.json"):
  9.     """读取配置,任何问题都回退到默认值"""
  10.     try:
  11.         text = Path(path).read_text(encoding="utf-8")
  12.         config = json.loads(text)
  13.     except FileNotFoundError:
  14.         print("配置文件不存在,使用默认配置")
  15.         return DEFAULT_CONFIG.copy()
  16.     except json.JSONDecodeError as e:
  17.         print(f"配置文件格式错误:{e},使用默认配置")
  18.         return DEFAULT_CONFIG.copy()
  19.     for key, value in DEFAULT_CONFIG.items():
  20.         config.setdefault(key, value)
  21.     return config
复制代码

几个实现要点:

- 把两个 except 分开写,是因为“文件不存在”和“JSON 格式错误”对使用者是完全不同的信息,合并成一句会丢失排查线索。
- 返回 DEFAULT_CONFIG.copy() 而不是默认字典本身,避免调用方修改返回值时污染模块级常量。
- 成功解析后再用 setdefault 补齐缺失键,这样旧版本配置文件也能正常启动,而且不会覆盖用户已有配置。
- 读取时显式指定 encoding="utf-8",规避不同平台默认编码差异带来的乱码问题。

九、常见坑速查

- 裸 except::会吞掉 KeyboardInterrupt,Ctrl+C 无法终止程序。
- except 类型过宽:掩盖真正的 bug,尽量捕获具体异常类型。
- 捕获了但不做任何处理:至少用 logging.exception(e) 留痕。
- finally 里 return:直接丢弃异常,排查时完全看不到报错。
- 用异常做正常流程控制:性能差且难读,正常分支该用 if。

十、小结

try/except/else/finally 四段各司其职:try 放风险语句,except 按类型精确捕获,else 放成功后的逻辑,finally 做资源清理;raise 主动抛错,from 建立异常链;业务错误用继承 Exception 的自定义异常类承载上下文;断言只用于内部不变量,正式校验交给 raise;整体风格上优先 EAFP 与 get/setdefault 这类安全方法。

可以拿来练手的三道题:写一个安全除法函数,处理除零与非数字输入,出错返回 None 并给出原因;自定义 PasswordTooShortError,密码少于 8 位时抛出;把一个 JSON 待办清单程序改造成文件损坏时给出友好提示而不是崩溃。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python异常处理try except完整结构与自定义异常

楼主整理得挺系统的,异常类型按触发场景归类这一点对新手特别友好,比单纯背列表好记多了。几个地方我也有同感:裸 except 确实是个大坑,以前写脚本时随手兜底,结果调试时连 Ctrl+C 都按不掉,找了半天才发现是把 KeyboardInterrupt 一起吞了;finally 里 return 会静默丢掉正在传播的异常,这个坑也很隐蔽,第一次见的时候盯着输出看了好一会儿才明白异常去哪了。还有裸 raise 和 raise e 的区别,保留原始 traceback 这点很关键,日志里能不能看到真正出错的那一行全靠它。用 from 建立异常链那段也实用,把 KeyError 换成更有业务含义的 ValueError 同时保留根因,排查配置问题时省事很多。 不过帖子好像到自定义异常那块就断了,InsufficientBalanceError 后面没了。标题里提到的断言、EAFP 风格和配置读取实战都还没看到,尤其配置读取那个实战例子挺想看看怎么把前面这些点串起来的,不知道是论坛吞了内容还是还没发完?如果方便的话期待补上,自定义异常里携带上下文属性的完整写法也想再看看。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python异常处理try except完整结构与自定义异常

这篇整理得很实用,尤其是把 except Exception、裸 except 和 finally 里 return 的坑讲清楚了。平时确实容易写 except: 一把梭,结果 Ctrl+C 都失效;裸 raise 和 raise ... from e 的区别也值得记住,排查问题能少走很多弯路。按触发场景记内置异常类型,比死背列表好理解。最后自定义异常那段好像还没贴完,期待把 __init__ 保存属性和调用父类构造的部分补上。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python异常处理try except完整结构与自定义异常

楼主整理得挺系统的,尤其是把“异常对象→捕获粒度→else/finally→raise 与异常链”这条主线捋清楚了,比只讲语法的人靠谱多了。 几点我特别认同: 一是裸 except 那段。实际项目里见过 Ctrl+C 停不掉服务的,排查半天发现就是某个循环里写了裸 except,把 KeyboardInterrupt 一起吞了。现在团队里我基本要求写 except Exception,而且兜底分支必须带日志。 二是 finally 里 return 的坑。这个真不是理论问题,之前有个函数在 finally 里写了 return,结果上层永远收不到异常,日志干干净净,问题拖了两天才定位到。另外 finally 里如果抛了新异常,也会把正在传播的异常覆盖掉,和 return 是同一类隐患。 三是裸 raise 和 raise e 的区别。很多人以为两者等价,其实裸 raise 保留原始 traceback,raise e 会从当前行重新起一个栈帧,排查时栈信息会多出一层干扰。用 from 显式链异常也很重要,尤其是配置解析、接口调用这类需要换语义的地方,把根因挂上去,报错信息才不至于只剩一句“配置错误”。 关于 else,我觉得它最大的价值就是楼主说的“避免被本段的 except 误捕获”。比如 try 里只放 open,else 里放处理逻辑,这样处理逻辑抛出的 ValueError
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-10 03:50 , Processed in 0.019859 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部