首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的异常处理把我坑惨了,原来try/except和try/finally的区别这么大

Python的异常处理把我坑惨了,原来try/except和try/finally的区别这么大

原创
作者头像
风一样的男子
发布于 2026-10-28 11:13:14
发布于 2026-10-28 11:13:14
90
举报
文章被收录于专栏:编程教程编程教程

小何那天写了一个数据库操作函数,需要从连接池拿连接、执行查询、然后归还连接。

他的代码大概是这样的:

代码语言:javascript
复制
def get_user(user_id):
    conn = pool.acquire()
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cursor.fetchone()
    except Exception as e:
        print(f"查询失败: {e}")
        conn.rollback()
        pool.release(conn)

测试的时候一切正常。用户存在时,返回用户数据;用户不存在时,捕获异常,打印错误。但上线跑了一周,连接池满了,服务直接卡死。

他查了半天才发现问题:当查询成功时,代码走到return cursor.fetchone()就返回了,pool.release(conn)根本没执行。连接只借不还,一周下来连接池自然被耗光。

小何很郁闷:“我明明写了try/except,为什么成功的时候连接没归还?”

同事看了一眼说:“你这里应该用finally。except只在出异常时执行,finally不管出不出异常都会执行。你把归还连接放在except里,成功的时候当然不执行。”

小何改成了:

代码语言:javascript
复制
def get_user(user_id):
    conn = pool.acquire()
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cursor.fetchone()
    except Exception as e:
        print(f"查询失败: {e}")
        conn.rollback()
        raise
    finally:
        pool.release(conn)

连接池再也没满过。

“同样是try后面跟的块,except和finally的执行时机差这么多?”

这个问题的答案,就是这篇文章要讲清楚的事:**except负责“出错时怎么办”,finally负责“无论如何都要做的事”。**

先看一个最直观的对比

代码语言:javascript
复制
def func_except():
    try:
        print("try 块执行")
        return "正常返回"
    except Exception:
        print("except 块执行")
    print("函数结束")

def func_finally():
    try:
        print("try 块执行")
        return "正常返回"
    finally:
        print("finally 块执行")

print("--- 调用 func_except ---")
print(func_except())

print("--- 调用 func_finally ---")
print(func_finally())

输出:

代码语言:javascript
复制
--- 调用 func_except ---
try 块执行
正常返回

--- 调用 func_finally ---
try 块执行
finally 块执行
正常返回

func_except里,try块正常执行,没有异常,except块不执行,函数直接返回。

func_finally里,try块正常执行,finally块照样执行,然后函数才真正返回。

关键区别:**except只在异常发生时执行,finally无论是否发生异常都执行。**

用一个生活场景来理解。你出门前要检查两件事:带伞、锁门。

try/except的做法是:如果下雨(异常),就带伞(处理异常)。不下雨就不带。它只处理“出问题”的情况。

try/finally的做法是:不管下不下雨,出门前都要锁门(清理资源)。下雨带伞是“条件动作”,锁门是“必做动作”。

区别一目了然:一个是有条件执行,一个是无条件执行。

try/except:捕获并处理异常

try/except的语义是:尝试执行try块,如果发生异常,跳到对应的except块处理。

代码语言:javascript
复制
try:
    result = 10 / 0
except ZeroDivisionError:
    print("除数不能为0")

try块里10 / 0抛出ZeroDivisionError,程序不会崩溃,而是跳到except块,打印错误信息,然后继续往下执行。

except可以捕获特定类型的异常,也可以捕获所有异常:

代码语言:javascript
复制
# 捕获特定异常
try:
    ...
except ValueError:
    ...

# 捕获多个异常
try:
    ...
except (ValueError, TypeError):
    ...

# 捕获所有异常(慎用)
try:
    ...
except Exception:
    ...

except块执行完后,程序继续往下走。如果你想让异常继续往上抛,需要显式raise:

代码语言:javascript
复制
try:
    ...
except ValueError as e:
    print(f"记录日志: {e}")
    raise  # 重新抛出,让上层知道出错了

try/except的核心作用是让程序在出错时能够恢复或者优雅降级,而不是直接崩溃。

try/finally:无论发生什么都要执行

try/finally的语义是:执行try块,不管成功还是失败,最后都执行finally块。

代码语言:javascript
复制
conn = pool.acquire()
try:
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchone()
finally:
    pool.release(conn)

这段代码的关键在于:不管try块里是正常返回、抛出异常、还是return/break/continue,finally块都会执行。

代码语言:javascript
复制
def test():
    try:
        return "try 返回"
    finally:
        print("finally 执行")

print(test())

输出:

代码语言:javascript
复制
finally 执行
try 返回

try块里的return会先“暂存”返回值,然后执行finally,最后才真正返回。finally的代码在return之后、函数真正返回之前执行。

如果finally里也有return,它会覆盖try里的返回值:

代码语言:javascript
复制
def test():
    try:
        return "try 返回"
    finally:
        return "finally 返回"

print(test())  # "finally 返回"

这种写法非常危险,会吞掉try块的返回值,甚至吞掉异常。实际代码里应该避免在finally里写return。

try/finally的核心作用是保证清理代码一定被执行,不管try块里发生了什么。

坑一:把清理代码放在 except 里

这就是小何踩的坑。

代码语言:javascript
复制
conn = pool.acquire()
try:
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchone()
except Exception as e:
    conn.rollback()
    pool.release(conn)  # 只有出异常时才归还

成功的时候,except不执行,连接不归还。失败的时候,连接归还了,但异常被吞掉了(没有raise),上层不知道出错了。

正确的做法是把清理代码放在finally里:

代码语言:javascript
复制
conn = pool.acquire()
try:
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchone()
except Exception as e:
    conn.rollback()
    raise
finally:
    pool.release(conn)

finally负责归还连接,不管成功失败都执行。except负责回滚和重新抛出异常,只在失败时执行。

清理代码放finally,错误处理放except。 两者的职责不能混。

坑二:以为 finally 只在异常时执行

有些人把finally当成“异常处理”的一部分,以为它只在出错时执行。实际上finally是无条件执行的,和是否出错无关。

代码语言:javascript
复制
try:
    print("正常执行")
finally:
    print("finally 执行")

输出:

代码语言:javascript
复制
正常执行
finally 执行

没有异常,finally照样执行。这就是它和except最根本的区别。

坑三:在 finally 里吞掉异常

finally块里如果抛出新的异常,会覆盖try块里的原始异常:

代码语言:javascript
复制
try:
    raise ValueError("原始错误")
finally:
    raise RuntimeError("清理时出错")

最终抛出来的是RuntimeError,ValueError丢失了。调试的时候你只能看到“清理时出错”,不知道最初发生了什么。

finally里的清理代码应该尽量保证不抛异常。如果可能抛异常,用try/except包起来:

代码语言:javascript
复制
finally:
    try:
        conn.close()
    except Exception:
        pass  # 记录日志,但不要抛出

坑四:在 finally 里 return

前面提过,finally里的return会覆盖try里的返回值,甚至吞掉异常:

代码语言:javascript
复制
def test():
    try:
        raise ValueError("出错了")
    finally:
        return "正常"

print(test())  # "正常",异常被吞了

这段代码不会抛ValueError,因为finally里的return把异常吞掉了。这比except吞异常更隐蔽,因为代码里根本没写except。

永远不要在finally里写return。 这是Python里少数几条“绝对不要”的规则之一。

坑五:except 捕获了不该捕获的异常

代码语言:javascript
复制
try:
    result = process(data)
except Exception:
    pass  # 静默忽略所有异常

这段代码捕获了所有异常,然后什么都不做。程序不会崩溃,但你永远不知道出了什么问题。

更糟糕的是,它连KeyboardInterrupt(用户按Ctrl+C)和SystemExit(程序退出)都捕获了,导致程序无法正常中断。

正确做法是捕获特定的异常,并且至少记录日志:

代码语言:javascript
复制
try:
    result = process(data)
except ValueError as e:
    logger.error(f"数据处理失败: {e}")
    raise
except KeyError as e:
    logger.error(f"缺少字段: {e}")
    return default_value

except Exception可以作为兜底,但一定要记录日志,并且在不需要继续处理时raise。

try/except/finally 组合使用

三者可以组合:

代码语言:javascript
复制
try:
    # 可能出错的代码
except SomeError:
    # 出错时的处理
finally:
    # 无论成败都要执行的清理

执行顺序:

  1. 执行try块。
  2. 如果发生异常,跳到匹配的except块。
  3. 不管是否发生异常,最后执行finally块。
  4. 如果except块里没有raise,异常被吞掉,程序继续往下走;如果raise了,异常继续往上抛。
  5. finally在异常抛出之前执行。
代码语言:javascript
复制
def test():
    try:
        print("1. try")
        raise ValueError("出错了")
    except ValueError as e:
        print(f"2. except: {e}")
        raise
    finally:
        print("3. finally")

try:
    test()
except ValueError:
    print("4. 上层捕获")

输出:

代码语言:javascript
复制
1. try
2. except: 出错了
3. finally
4. 上层捕获

finally在异常传到上层之前执行。这个顺序保证了资源在异常传播之前被清理。

那 else 呢?

try还可以跟else块。else在try块没有发生异常时执行:

代码语言:javascript
复制
try:
    result = process(data)
except ValueError:
    print("处理失败")
else:
    print("处理成功")
    save(result)

else和except是互斥的:try成功走else,失败走except。finally不管成败都走。

else的使用场景比较少。它适合“只有try成功时才需要执行的代码”,而且这段代码本身不应该被try的异常处理覆盖。但大多数时候,直接把代码写在try块最后一行也一样,所以else在实际项目中不常见。

上下文管理器:try/finally 的语法糖

with语句本质上就是try/finally的封装:

代码语言:javascript
复制
with open("file.txt") as f:
    content = f.read()

等价于:

代码语言:javascript
复制
f = open("file.txt")
try:
    content = f.read()
finally:
    f.close()

with语句自动处理了finally里的清理逻辑。对于文件、连接、锁这些需要“用完必须释放”的资源,优先用with,而不是手动写try/finally。

代码语言:javascript
复制
# 推荐
with pool.acquire() as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchone()

# 不推荐(除非连接池不支持上下文管理器)
conn = pool.acquire()
try:
    ...
finally:
    pool.release(conn)

with不仅更简洁,而且不容易忘写finally。

什么时候用 try/except,什么时候用 try/finally?

判断标准很清晰:

需要处理错误(记录日志、降级、重试、转换异常):用try/except。

需要保证清理代码一定执行(关闭文件、归还连接、释放锁):用try/finally。

两者都需要:用try/except/finally。except处理错误,finally清理资源。

资源可以用with管理:优先用with,它自动处理finally。

不需要处理错误,也不需要清理:不要写try。裸的try/finally只适合清理场景,裸的try/except只适合处理场景。

回到小何的故事

小何最终的代码是这样的:

代码语言:javascript
复制
def get_user(user_id):
    conn = pool.acquire()
    try:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cursor.fetchone()
    except Exception as e:
        conn.rollback()
        raise
    finally:
        pool.release(conn)

成功时,try块返回数据,finally归还连接。失败时,except回滚并重新抛出异常,finally归还连接。两种情况下连接都会被归还。

他后来在团队文档里写了一条:“except管错误处理,finally管资源清理。清理代码放finally,别放except。except只在出错时执行,成功的时候不执行,放清理代码会漏。finally无条件执行,适合关文件、还连接、释放锁。能用with就用with,别手写try/finally。”

这个区分看起来简单,但真到写代码的时候,很多人会把“出错时回滚”和“用完归还”混在一起,全塞进except里。成功路径没清理,资源泄漏;失败路径吞异常,问题被掩盖。

记住那个出门的比喻:except是“下雨带伞”,只有下雨才做;finally是“出门锁门”,不管下不下雨都要做。你把锁门写在“下雨带伞”的分支里,晴天出门就没锁门。

异常处理本身不难,难的是想清楚“这段代码是出错才执行,还是无论如何都要执行”。想清楚了,try/except和try/finally就不再是坑,而是两个职责分明的工具。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

目录
  • 先看一个最直观的对比
  • try/except:捕获并处理异常
  • try/finally:无论发生什么都要执行
  • 坑一:把清理代码放在 except 里
  • 坑二:以为 finally 只在异常时执行
  • 坑三:在 finally 里吞掉异常
  • 坑四:在 finally 里 return
  • 坑五:except 捕获了不该捕获的异常
  • try/except/finally 组合使用
  • 那 else 呢?
  • 上下文管理器:try/finally 的语法糖
  • 什么时候用 try/except,什么时候用 try/finally?
  • 回到小何的故事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档