首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python异常处理这件事,90%的人都在用错误的方式

Python异常处理这件事,90%的人都在用错误的方式

作者头像
用户11081884
发布2026-07-20 20:33:40
发布2026-07-20 20:33:40
200
举报

看到一段代码:

代码语言:javascript
复制
try:
    result = do_something()
except:
    pass

我说这个except把什么都吞了,出bug你怎么查?对方一脸无辜:不是加了异常处理了吗?

……行吧。

异常处理看着简单,就是 try...except 嘛,谁不会写?但你翻翻自己项目里的代码,我赌五毛,下面这些问题至少中两条。


一、except: — 什么都抓,等于什么都没抓

代码语言:javascript
复制
try:
    data = json.loads(raw_text)
except:
    pass

看起来“很安全”,不会崩。但问题在于,你连程序出错了都不知道

except: 不带类型,会捕获一切异常。真正该处理的 JSONDecodeError 抓了,不该捕获的 KeyboardInterrupt(Ctrl+C)也抓了,SystemExit 也抓了——你连退出程序都退不了。

更要命的是 pass。出了错不记日志、不重试、不告警,业务逻辑悄无声息地跳过去了。线上出了问题,日志里干干净净,你连排查方向都没有。

我之前就踩过这个坑。一个Kafka消费服务,线上隔三差五丢消息,查了两天,最后发现就是 except: pass 把反序列化错误吞了。Kafka offset正常提交,业务什么也没执行。

改成这样:

代码语言:javascript
复制
try:
    data = json.loads(raw_text)
except json.JSONDecodeError as e:
    logger.error(f"JSON解析失败: {e}, 原始内容: {raw_text[:200]}")
    # 根据业务决定:重试 or 丢进死信队列
    raise

至少出了错你能看到是什么错。


二、except Exception — 抓太宽,该崩的没崩

比裸 except: 好一点,但也就好那么一点:

代码语言:javascript
复制
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 里面抛了个数据库连接超时的异常,这段代码照单全收,然后返回一个“下单失败”。

但钱扣了没有?你不知道。可能扣了也可能没扣,用户那边显示“下单失败”,银行那边钱已经划走了。

这种涉及多步操作的业务,你不能一把全兜住。有些异常就是该让它崩的,崩了至少监控能发现;吞了反而掩盖了更严重的问题。

应该这么写,把不同环节分开处理:

代码语言:javascript
复制
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还不报错,属于那种“写了但白写”的类型:

代码语言:javascript
复制
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}")

发现问题了吗?NotFoundErrorAppError 的子类,但写在后面。Python按顺序匹配,先匹配到 AppError,后面的 NotFoundError 永远执行不到。

Python倒也不会报错,顶多给你一个淡淡的warning,你大概率看不到。

顺序换过来就行了:

代码语言:javascript
复制
try:
    get_resource(id)
except NotFoundError as e:
    logger.warning(f"资源不存在: {e}")
except AppError as e:
    logger.error(f"其他应用异常: {e}")

很多人写 except 的时候压根没想过继承关系,反正都写上就完事。但这里有个原则你得记着:从具体到一般,子类在前,父类在后。 写反了等于白写。


四、拿异常当流程控制 — 看着高级,实际最蠢

这种写法在项目里出现频率极高:

代码语言:javascript
复制
def get_user(user_id):
    try:
        user = db.query(User).get(user_id)
        return user
    except:
        return None

逻辑上说得通:查不到就返回None。但你仔细想想,db.query 还可能因为连接超时、SQL语法错误、权限问题抛异常,这些全被当成“查不到”处理了。

更典型的例子:

代码语言:javascript
复制
try:
    value = config[key]
except KeyError:
    value = default_value

Python自己都有更地道的写法:

代码语言:javascript
复制
value = config.get(key, default_value)

用异常控制流程的问题在于:异常是给“异常情况”用的,不是给“正常情况”用的。查不到数据是业务常态,不是异常;数据库挂了才是异常。

区分清楚:

代码语言:javascript
复制
# 查不到是正常业务逻辑 → 用返回值判断
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

别把“找不到”和“出错了”混为一谈。


五、丢了原始异常 — 排查时的噩梦

这个坑属于“不遇到不知道,遇到了想骂人”:

代码语言:javascript
复制
try:
    result = call_downstream_api(url)
except requests.RequestException as e:
    raise Exception(f"调用下游服务失败")

你看出来了吗?raise Exception 把原始异常 e 丢了。下游返回的HTTP状态码是什么?超时还是连接被拒?一概不知。你只看到一句干巴巴的“调用下游服务失败”,然后两眼一黑。

两种保留原始异常的方式:

代码语言:javascript
复制
# 方式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的语法,保留完整的异常链。排查的时候你能看到:

代码语言:javascript
复制
ServiceError: 调用下游服务失败: /api/orders
The above exception was the direct cause of the following exception:
requests.ConnectionError: Connection timed out after 30s

一眼就知道是超时还是连接被拒。丢掉原始异常信息?排查的时候你会恨自己。


六、不用 finally — 资源泄漏的温床

看这段代码:

代码语言:javascript
复制
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

代码语言:javascript
复制
# 方式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

很多人意识到该用自定义异常了,但写出来的效果是这样的:

代码语言:javascript
复制
class MyError(Exception):
    pass

class AnotherError(Exception):
    pass

# 然后该怎样还是怎样
raise Exception("something went wrong")

自定义异常建了一堆,用的时候还是 raise Exception。这跟买了健身卡从来不去是一个道理。

自定义异常的意义在于:让调用方能区分不同类型的错误,做出不同的处理。你得先想清楚异常的分类体系,再去写定义:

代码语言:javascript
复制
# 基础异常
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}")

这样调用方才能针对性处理:

代码语言:javascript
复制
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

自定义异常不是摆设。你定义了就得用,而且继承关系得理清楚,不然还不如不定义。

异常处理说白了就几条底线,不难理解,难的是每次写代码都绷着这根弦:

  1. 能处理的才捕获,处理不了就让它崩。动不动就 except Exception,你以为你在兜底,其实你在埋雷。
  2. 别吞异常。你不记日志,出bug就靠猜。猜不到的,就靠加班。
  3. 保留异常链。raise ... from e 是Python 3给的礼物,别浪费。丢掉原始异常信息的人,排查的时候会恨自己。

赶进度的时候谁不想 except: pass 一下然后赶紧写下一个功能?但那种技术债,迟早要还的。

“无他,惟手熟尔”!有需要的用起来!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nicholas与Pypi 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、except: — 什么都抓,等于什么都没抓
  • 二、except Exception — 抓太宽,该崩的没崩
  • 三、异常顺序写反了 — 子类必须在前
  • 四、拿异常当流程控制 — 看着高级,实际最蠢
  • 五、丢了原始异常 — 排查时的噩梦
  • 六、不用 finally — 资源泄漏的温床
  • 七、自定义异常乱写 — 最终还是 raise Exception
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档