
看到一段代码:
try:result = do_something()except:pass
我说这个except把什么都吞了,出bug你怎么查?对方一脸无辜:不是加了异常处理了吗?
……行吧。
异常处理看着简单,就是 try...except 嘛,谁不会写?但你翻翻自己项目里的代码,我赌五毛,下面这些问题至少中两条。
往期阅读>>>
Python 自动化管理Jenkins的15个实用脚本,提升效率
App2Docker:如何无需编写Dockerfile也可以创建容器镜像
Python 自动化识别Nginx配置并导出为excel文件,提升Nginx管理效率
except: — 什么都抓,等于什么都没抓try:data = json.loads(raw_text)except:pass
看起来“很安全”,不会崩。但问题在于,你连程序出错了都不知道。
except: 不带类型,会捕获一切异常。真正该处理的 JSONDecodeError 抓了,不该捕获的 KeyboardInterrupt(Ctrl+C)也抓了,SystemExit 也抓了——你连退出程序都退不了。
更要命的是 pass。出了错不记日志、不重试、不告警,业务逻辑悄无声息地跳过去了。线上出了问题,日志里干干净净,你连排查方向都没有。
我之前就踩过这个坑。一个Kafka消费服务,线上隔三差五丢消息,查了两天,最后发现就是 except: pass 把反序列化错误吞了。Kafka offset正常提交,业务什么也没执行。
改成这样:
try:data = json.loads(raw_text)except json.JSONDecodeError as e:logger.error(f"JSON解析失败: {e}, 原始内容: {raw_text[:200]}")# 根据业务决定:重试 or 丢进死信队列raise
至少出了错你能看到是什么错。
except Exception — 抓太宽,该崩的没崩比裸 except: 好一点,但也就好那么一点:
try:order = create_order(user_id, items)payment = charge_payment(order)send_notification(user_id, order)except Exception as e:logger.error(f"下单失败: {e}")return {"status": "error"}
看起来没毛病?想想这个场景:charge_payment 里面抛了个数据库连接超时的异常,这段代码照单全收,然后返回一个“下单失败”。
但钱扣了没有?你不知道。可能扣了也可能没扣,用户那边显示“下单失败”,银行那边钱已经划走了。
这种涉及多步操作的业务,你不能一把全兜住。有些异常就是该让它崩的,崩了至少监控能发现;吞了反而掩盖了更严重的问题。
应该这么写,把不同环节分开处理:
try:order = create_order(user_id, items)except ValidationError as e:# 业务校验失败,可以优雅处理logger.warning(f"订单校验失败: {e}")return {"status": "error", "msg": str(e)}# 支付环节的异常不该在这里吞掉# 让它冒泡上去,由上层的全局异常处理器捕获和告警payment = charge_payment(order)send_notification(user_id, order)
一句话:能处理的异常才捕获,处理不了的就让它崩。胡乱兜底比不兜底更危险。
这个坑藏得深,Python还不报错,属于那种“写了但白写”的类型:
class AppError(Exception):passclass NotFoundError(AppError):passtry:get_resource(id)except AppError as e:logger.error(f"应用异常: {e}")except NotFoundError as e:logger.warning(f"资源不存在: {e}")
发现问题了吗?NotFoundError 是 AppError 的子类,但写在后面。Python按顺序匹配,先匹配到 AppError,后面的 NotFoundError 永远执行不到。
Python倒也不会报错,顶多给你一个淡淡的warning,你大概率看不到。
顺序换过来就行了:
try:get_resource(id)except NotFoundError as e:logger.warning(f"资源不存在: {e}")except AppError as e:logger.error(f"其他应用异常: {e}")
很多人写 except 的时候压根没想过继承关系,反正都写上就完事。但这里有个原则你得记着:从具体到一般,子类在前,父类在后。 写反了等于白写。
这种写法在项目里出现频率极高:
def get_user(user_id):try:user = db.query(User).get(user_id)return userexcept:return None
逻辑上说得通:查不到就返回None。但你仔细想想,db.query 还可能因为连接超时、SQL语法错误、权限问题抛异常,这些全被当成“查不到”处理了。
更典型的例子:
try:value = config[key]except KeyError:value = default_value
Python自己都有更地道的写法:
value = config.get(key, default_value)用异常控制流程的问题在于:异常是给“异常情况”用的,不是给“正常情况”用的。查不到数据是业务常态,不是异常;数据库挂了才是异常。
区分清楚:
# 查不到是正常业务逻辑 → 用返回值判断user = db.query(User).filter_by(id=user_id).first()if user is None:return Nonereturn user# 真正的异常 → 用try/excepttry:user = db.query(User).filter_by(id=user_id).one()except DatabaseError as e:logger.exception(f"数据库异常: {e}")raise
别把“找不到”和“出错了”混为一谈。
这个坑属于“不遇到不知道,遇到了想骂人”:
try:result = call_downstream_api(url)except requests.RequestException as e:raise Exception(f"调用下游服务失败")
你看出来了吗?raise Exception 把原始异常 e 丢了。下游返回的HTTP状态码是什么?超时还是连接被拒?一概不知。你只看到一句干巴巴的“调用下游服务失败”,然后两眼一黑。
两种保留原始异常的方式:
# 方式1:raise from(推荐,链式异常)try:result = call_downstream_api(url)except requests.RequestException as e:raise ServiceError(f"调用下游服务失败: {url}") from e# 方式2:直接raise原始异常try:result = call_downstream_api(url)except requests.RequestException:logger.exception(f"调用下游服务失败: {url}")raise
raise ... from e 是Python 3的语法,保留完整的异常链。排查的时候你能看到:
ServiceError: 调用下游服务失败: /api/ordersThe above exception was the direct cause of the following exception:requests.ConnectionError: Connection timed out after 30s
一眼就知道是超时还是连接被拒。丢掉原始异常信息?排查的时候你会恨自己。
finally — 资源泄漏的温床看这段代码:
def process_file(path):f = open(path, 'r')data = f.read()result = transform(data)f.close()return result
如果 transform() 抛了异常呢?f.close() 永远不会执行。文件描述符就泄漏了。跑个长驻服务,泄漏积累到一定程度,Too many open files 的报错就来了。
用 finally 或者直接 with:
# 方式1:with语句(最简洁)def process_file(path):with open(path, 'r') as f:data = f.read()result = transform(data)return result# 方式2:finally(需要更复杂的清理逻辑时)def process_file(path):f = open(path, 'r')try:data = f.read()return transform(data)finally:f.close() # 无论是否异常都会执行
finally 不只是关文件。数据库连接、网络连接、临时文件清理,都该放进去。不管代码正常返回还是抛异常,清理逻辑必须执行——这不是风格偏好,是生存技能。
raise Exception很多人意识到该用自定义异常了,但写出来的效果是这样的:
class MyError(Exception):passclass AnotherError(Exception):pass# 然后该怎样还是怎样raise Exception("something went wrong")
自定义异常建了一堆,用的时候还是 raise Exception。这跟买了健身卡从来不去是一个道理。
自定义异常的意义在于:让调用方能区分不同类型的错误,做出不同的处理。你得先想清楚异常的分类体系,再去写定义:
# 基础异常class AppError(Exception):"""应用层异常基类"""pass# 业务异常(调用方可以优雅处理)class ValidationError(AppError):def __init__(self, field, message):self.field = fieldself.message = messagesuper().__init__(f"{field}: {message}")class NotFoundError(AppError):def __init__(self, resource, resource_id):self.resource = resourceself.resource_id = resource_idsuper().__init__(f"{resource} [{resource_id}] 不存在")# 基础设施异常(需要告警,可能需要重试)class InfrastructureError(AppError):passclass DatabaseError(InfrastructureError):passclass ExternalServiceError(InfrastructureError):def __init__(self, service, original_error):self.service = serviceself.original_error = original_errorsuper().__init__(f"外部服务 {service} 调用失败: {original_error}")
这样调用方才能针对性处理:
try:order = create_order(data)except ValidationErrorase:return {"status": "fail", "msg": e.message}, 400except NotFoundErrorase:return {"status": "fail", "msg": "资源不存在"}, 404except InfrastructureErrorase:logger.exception("基础设施异常")return {"status": "error", "msg": "服务暂时不可用"}, 503
自定义异常不是摆设。你定义了就得用,而且继承关系得理清楚,不然还不如不定义。
异常处理说白了就几条底线,不难理解,难的是每次写代码都绷着这根弦:
能处理的才捕获,处理不了就让它崩。动不动就 except Exception,你以为你在兜底,其实你在埋雷。
别吞异常。你不记日志,出bug就靠猜。猜不到的,就靠加班。
保留异常链。raise ... from e 是Python 3给的礼物,别浪费。丢掉原始异常信息的人,排查的时候会恨自己。
赶进度的时候谁不想 except: pass 一下然后赶紧写下一个功能?但那种技术债,迟早要还的。
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c
