具体异常和多个处理分支
try 里只放你预期会失败的那一步。把用户输入的文本转成数字,就捕 ValueError。异常的细节对用户有用时,用 as e 把它绑定下来。超时该重试、连接断了该报警,这两种就分开写处理分支。
try:
age = int(user_input)
except ValueError:
print("that wasn't a number")
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"bad JSON at line {e.lineno}: {e.msg}")
try:
resp = requests.get(url, timeout=5)
except requests.Timeout:
retry()
except requests.ConnectionError:
alert_ops()
except (ValueError, KeyError) as e:
log(e)
把几个异常类写成一个元组,只有在它们的处理方式完全一样时才合适。恢复方式不同,就分开写 except,让策略留在明面上。
else 和 finally
else 只在 try 成功时执行。它把后续处理挪到受保护的块外面,这样 process 里的 bug 就不会被误认成打开文件失败。finally 不管成功、失败还是提前返回都会执行,所以适合放释放锁这类无条件的清理。
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print("no such file")
else:
process(f.read())
f.close()
finally:
release_lock()
对文件来说,with 语句通常比手动 close 更稳。finally 最有价值的场景是资源没有上下文管理器,或者清理动作跨了好几步操作。
抛出、重新抛出,和重试
调用方给了一个你的函数没法接受的值,就抛 ValueError。在 except 里写一个裸的 raise,会把同一个异常连着原始 traceback 一起往上送。做重试时只捕获临时性的失败,每失败一次就多等一会儿,最后一次尝试之后重新抛出。
def set_age(age):
if age < 0:
raise ValueError(f"age can't be negative, got {age}")
try:
charge_card(order)
except PaymentError:
log_incident(order)
raise # same error and traceback
class QuotaExceeded(Exception):
pass
for attempt in range(3):
try:
result = flaky_api_call()
break
except requests.Timeout:
if attempt == 2:
raise
time.sleep(2 ** attempt) # 1 second, then 2
不要重试校验错误、权限失败,也不要重试刷卡被拒。反复重试一个永久性的失败只会拖时间,还可能把副作用做重一遍。重试策略应该属于那个知道再试一次安不安全的操作。
修掉静默捕获和骗人的 traceback
典型症状是,代码跑完了,但什么都没发生。去搜 except 冒号 pass,或者 except Exception 冒号 pass。这些行会把真正的 traceback 丢掉,包括 NameError: name 'user_id' is not defined 这类消息。调试期间先把这个捕获删掉,或者用 logger.exception 把 traceback 记下来。
NameError: name 'user_id' is not defined
try:
save_user(user_id)
except Exception:
pass # hides the NameError
# If a top-level worker truly must stay alive:
try:
run_one_job()
except Exception:
logger.exception("job failed") # records full traceback
先临时删掉这个宽泛的处理分支,重跑一遍,把原始 traceback 露出来。修掉它点名的那一行。然后只捕获程序真的能恢复的那个具体异常。如果是在进程边界上,宽泛捕获确实是有意为之,那就用
logger.exception(...);它和 logger.error(e) 不一样,会带上 traceback。裸 except 的范围还要更大:连 KeyboardInterrupt 这类信号都会被它吃掉。不要为了保险把整个函数包起来。让错误往上冒,直到某一层能做出真正的决定:重试、报给用户,还是停下来。