
小何那天写了一个数据库操作函数,需要从连接池拿连接、执行查询、然后归还连接。
他的代码大概是这样的:
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里,成功的时候当然不执行。”
小何改成了:
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负责“无论如何都要做的事”。**
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())输出:
--- 调用 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:
result = 10 / 0
except ZeroDivisionError:
print("除数不能为0")try块里10 / 0抛出ZeroDivisionError,程序不会崩溃,而是跳到except块,打印错误信息,然后继续往下执行。
except可以捕获特定类型的异常,也可以捕获所有异常:
# 捕获特定异常
try:
...
except ValueError:
...
# 捕获多个异常
try:
...
except (ValueError, TypeError):
...
# 捕获所有异常(慎用)
try:
...
except Exception:
...except块执行完后,程序继续往下走。如果你想让异常继续往上抛,需要显式raise:
try:
...
except ValueError as e:
print(f"记录日志: {e}")
raise # 重新抛出,让上层知道出错了try/except的核心作用是让程序在出错时能够恢复或者优雅降级,而不是直接崩溃。
try/finally的语义是:执行try块,不管成功还是失败,最后都执行finally块。
conn = pool.acquire()
try:
cursor = conn.cursor()
cursor.execute("SELECT ...")
return cursor.fetchone()
finally:
pool.release(conn)这段代码的关键在于:不管try块里是正常返回、抛出异常、还是return/break/continue,finally块都会执行。
def test():
try:
return "try 返回"
finally:
print("finally 执行")
print(test())输出:
finally 执行
try 返回try块里的return会先“暂存”返回值,然后执行finally,最后才真正返回。finally的代码在return之后、函数真正返回之前执行。
如果finally里也有return,它会覆盖try里的返回值:
def test():
try:
return "try 返回"
finally:
return "finally 返回"
print(test()) # "finally 返回"这种写法非常危险,会吞掉try块的返回值,甚至吞掉异常。实际代码里应该避免在finally里写return。
try/finally的核心作用是保证清理代码一定被执行,不管try块里发生了什么。
这就是小何踩的坑。
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里:
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是无条件执行的,和是否出错无关。
try:
print("正常执行")
finally:
print("finally 执行")输出:
正常执行
finally 执行没有异常,finally照样执行。这就是它和except最根本的区别。
finally块里如果抛出新的异常,会覆盖try块里的原始异常:
try:
raise ValueError("原始错误")
finally:
raise RuntimeError("清理时出错")最终抛出来的是RuntimeError,ValueError丢失了。调试的时候你只能看到“清理时出错”,不知道最初发生了什么。
finally里的清理代码应该尽量保证不抛异常。如果可能抛异常,用try/except包起来:
finally:
try:
conn.close()
except Exception:
pass # 记录日志,但不要抛出前面提过,finally里的return会覆盖try里的返回值,甚至吞掉异常:
def test():
try:
raise ValueError("出错了")
finally:
return "正常"
print(test()) # "正常",异常被吞了这段代码不会抛ValueError,因为finally里的return把异常吞掉了。这比except吞异常更隐蔽,因为代码里根本没写except。
永远不要在finally里写return。 这是Python里少数几条“绝对不要”的规则之一。
try:
result = process(data)
except Exception:
pass # 静默忽略所有异常这段代码捕获了所有异常,然后什么都不做。程序不会崩溃,但你永远不知道出了什么问题。
更糟糕的是,它连KeyboardInterrupt(用户按Ctrl+C)和SystemExit(程序退出)都捕获了,导致程序无法正常中断。
正确做法是捕获特定的异常,并且至少记录日志:
try:
result = process(data)
except ValueError as e:
logger.error(f"数据处理失败: {e}")
raise
except KeyError as e:
logger.error(f"缺少字段: {e}")
return default_valueexcept Exception可以作为兜底,但一定要记录日志,并且在不需要继续处理时raise。
三者可以组合:
try:
# 可能出错的代码
except SomeError:
# 出错时的处理
finally:
# 无论成败都要执行的清理执行顺序:
try块。except块。finally块。except块里没有raise,异常被吞掉,程序继续往下走;如果raise了,异常继续往上抛。finally在异常抛出之前执行。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. 上层捕获")输出:
1. try
2. except: 出错了
3. finally
4. 上层捕获finally在异常传到上层之前执行。这个顺序保证了资源在异常传播之前被清理。
try还可以跟else块。else在try块没有发生异常时执行:
try:
result = process(data)
except ValueError:
print("处理失败")
else:
print("处理成功")
save(result)else和except是互斥的:try成功走else,失败走except。finally不管成败都走。
else的使用场景比较少。它适合“只有try成功时才需要执行的代码”,而且这段代码本身不应该被try的异常处理覆盖。但大多数时候,直接把代码写在try块最后一行也一样,所以else在实际项目中不常见。
with语句本质上就是try/finally的封装:
with open("file.txt") as f:
content = f.read()等价于:
f = open("file.txt")
try:
content = f.read()
finally:
f.close()with语句自动处理了finally里的清理逻辑。对于文件、连接、锁这些需要“用完必须释放”的资源,优先用with,而不是手动写try/finally。
# 推荐
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/finally。except处理错误,finally清理资源。
资源可以用with管理:优先用with,它自动处理finally。
不需要处理错误,也不需要清理:不要写try。裸的try/finally只适合清理场景,裸的try/except只适合处理场景。
小何最终的代码是这样的:
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 删除。