
看到一段代码:
try:
result = do_something()
except:
pass我说这个except把什么都吞了,出bug你怎么查?对方一脸无辜:不是加了异常处理了吗?
……行吧。
异常处理看着简单,就是 try...except 嘛,谁不会写?但你翻翻自己项目里的代码,我赌五毛,下面这些问题至少中两条。
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):
pass
class NotFoundError(AppError):
pass
try:
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 user
except:
return None逻辑上说得通:查不到就返回None。但你仔细想想,db.query 还可能因为连接超时、SQL语法错误、权限问题抛异常,这些全被当成“查不到”处理了。
更典型的例子:
try:
value = config[key]
except KeyError:
value = default_valuePython自己都有更地道的写法:
value = config.get(key, default_value)用异常控制流程的问题在于:异常是给“异常情况”用的,不是给“正常情况”用的。查不到数据是业务常态,不是异常;数据库挂了才是异常。
区分清楚:
# 查不到是正常业务逻辑 → 用返回值判断
user = db.query(User).filter_by(id=user_id).first()
if user is None:
return None
return user
# 真正的异常 → 用try/except
try:
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}")
raiseraise ... from e 是Python 3的语法,保留完整的异常链。排查的时候你能看到:
ServiceError: 调用下游服务失败: /api/orders
The 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):
pass
class AnotherError(Exception):
pass
# 然后该怎样还是怎样
raise Exception("something went wrong")自定义异常建了一堆,用的时候还是 raise Exception。这跟买了健身卡从来不去是一个道理。
自定义异常的意义在于:让调用方能区分不同类型的错误,做出不同的处理。你得先想清楚异常的分类体系,再去写定义:
# 基础异常
class AppError(Exception):
"""应用层异常基类"""
pass
# 业务异常(调用方可以优雅处理)
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NotFoundError(AppError):
def __init__(self, resource, resource_id):
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource} [{resource_id}] 不存在")
# 基础设施异常(需要告警,可能需要重试)
class InfrastructureError(AppError):
pass
class DatabaseError(InfrastructureError):
pass
class ExternalServiceError(InfrastructureError):
def __init__(self, service, original_error):
self.service = service
self.original_error = original_error
super().__init__(f"外部服务 {service} 调用失败: {original_error}")这样调用方才能针对性处理:
try:
order = create_order(data)
except ValidationErrorase:
return {"status": "fail", "msg": e.message}, 400
except NotFoundErrorase:
return {"status": "fail", "msg": "资源不存在"}, 404
except InfrastructureErrorase:
logger.exception("基础设施异常")
return {"status": "error", "msg": "服务暂时不可用"}, 503自定义异常不是摆设。你定义了就得用,而且继承关系得理清楚,不然还不如不定义。
异常处理说白了就几条底线,不难理解,难的是每次写代码都绷着这根弦:
except Exception,你以为你在兜底,其实你在埋雷。raise ... from e 是Python 3给的礼物,别浪费。丢掉原始异常信息的人,排查的时候会恨自己。赶进度的时候谁不想 except: pass 一下然后赶紧写下一个功能?但那种技术债,迟早要还的。
“无他,惟手熟尔”!有需要的用起来!
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!