周五晚上的噩梦
去年周五晚上上线,一个小模块报错。我图省事写了except:,以为万事大吉。结果静默吞掉了数据库连接异常,数据丢了半小时才发现。我在公司待到凌晨三点。😂
# 我的作死代码
defsave_data(data):
try:
db.insert(data)
except: # 千万别这么干
pass
裸except是黑洞
except:捕获什么?KeyboardInterrupt、SystemExit、GeneratorExit,全吞。Ctrl+C退不出,用户以为死机。不是异常处理,是黑洞。
# 捕获范围层级
BaseException
├── SystemExit <- 不该抓
├── KeyboardInterrupt <- 不该抓
├── GeneratorExit <- 不该抓
└── Exception <- 抓这个
具体异常是底线
说点得罪人的:写except Exception都比你写裸except强。但最好再具体点。try/except是为了处理预期内的错误,不是给代码打麻醉。
import requests
deffetch(url):
try:
resp = requests.get(url, timeout=5)
resp.raise_for_status()
return resp.json()
except requests.exceptions.Timeout:
print("超时,换备用URL")
return fetch_backup(url)
except requests.exceptions.HTTPError as e:
print(f"服务器抽风: {e}")
returnNone
else放主逻辑
很多人只用try/except,把else和finally当空气。else是try没异常时才执行,放主逻辑;finally无论咋样都执行,放清理代码。分工明确。
defread_config(path):
f = None
try:
f = open(path, 'r')
except FileNotFoundError:
print("配置丢了,用默认的")
return {}
else:
import json
return json.load(f)
finally:
if f:
f.close()
with语句更偷懒
上面那段可以更懒。with自动调finally,语义化更强。文件、锁、连接,我现在无脑with。代码少一半,安心多一倍。
defread_config(path):
try:
with open(path, 'r') as f:
import json
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"读取失败: {e}")
return {}
别再用字符串raise
我见过最离谱的:raise一个字符串。raise "出错了"。Python2里居然能跑,Python3终于禁了。自定义异常类才是正路。
classBizError(Exception):
"""业务异常基类"""
pass
classInsufficientBalance(BizError):
def__init__(self, uid, amount, balance):
self.uid = uid
self.amount = amount
self.balance = balance
super().__init__(
f"用户{uid}余额不足,"
f"需要{amount},只有{balance}"
)
defdeduct(uid, amount):
balance = get_balance(uid)
if balance < amount:
raise InsufficientBalance(uid, amount, balance)
do_deduct(uid, amount)
异常也能批量抓
有时多个异常同一种处理逻辑,可以用元组批量捕获。代码清爽,不会漏。这比写一堆重复的except块优雅多了。
try:
risky_operation()
except (ConnectionError, TimeoutError, SSLError):
logger.warning("网络相关错误,准备重试")
retry()
异常链保留现场
封装底层异常时,原始堆栈丢了最痛。raise from把异常链串起来,追查问题一目了然。别让你的异常变成无头案。
defparse_user(data):
try:
return json.loads(data)
except json.JSONDecodeError as e:
raise ValueError(
f"用户数据解析失败: {data[:50]}..."
) from e
# traceback 显示两条链:
# 1. json.JSONDecodeError
# 2. ValueError 用户数据解析失败...
异常不是goto
我见过把异常当流程控制用的。循环里抛异常跳出,判断分支用raise。求你了,异常是处理异常情况的。性能差,语义烂。
# 烂代码
values = [1,2,3]
try:
for v in values:
if v > 5:
raise StopIteration
except StopIteration:
result = v
# 正常人
result = next((v for v in values if v > 5), None)
日志别用print
异常捕获了不记录,等于没捕获。用logging,级别选对:预期内用warning,意料外用error,带着traceback一起记。
import logging
logger = logging.getLogger(__name__)
defdivide(a, b):
try:
return a / b
except ZeroDivisionError:
logger.error("除零错误", exc_info=True)
return float('inf')
except TypeError:
logger.exception("参数类型不对: %s, %s", a, b)
returnNone
我的四连问
现在写代码我会扫一眼:具体异常类型写了吗?else/finally用了吗?异常链保留了吗?会不会静默吞错误?这几问下来,能避开八成坑。
+----------------+------------------------+
| 反模式 | 正确做法 |
+----------------+------------------------+
| except: | except SpecificError: |
| 裸pass | log + 处理/重抛 |
| raise "字符串" | raise CustomError(msg) |
| 吞掉所有异常 | 只捕获能处理的 |
| 异常当if用 | 正常流程控制 |
+----------------+------------------------+