7
异常处理:别让程序一句话崩
try / except / with
程序跑飞了不可怕,可怕的是它"啪"一下红字退出,用户一脸懵。异常处理就是"提前告诉 Python:出事了走哪条退路"。写得好的程序,遇到错误会提示、会重试、会优雅地收尾。
try / except / else / finally
try:
n = int(input("输入一个数字:"))
result = 100 / n
except ValueError:
print("你输入的不是数字!")
except ZeroDivisionError:
print("不能除以 0!")
else:
print(f"100 / {n} = {result}") # 没出错才走这里
finally:
print("无论如何都执行(关文件、关连接)")
常见异常类型
| 异常 | 什么时候炸 |
ValueError | int("abc") 这种类型对但值不对。 |
TypeError | 类型不对,比如 "a" + 1。 |
KeyError | 字典取了不存在的键。 |
IndexError | 列表下标越界。 |
FileNotFoundError | 打开不存在的文件。 |
ZeroDivisionError | 除以 0。 |
自定义异常与 raise
class AuthError(Exception):
"""登录失败时抛这个错"""
pass
def login(user, pwd):
if user != "admin":
raise AuthError("用户名不对")
if pwd != "123456":
raise AuthError("密码不对")
try:
login("root", "xxx")
except AuthError as e:
print("登录失败:", e)
# 登录失败: 用户名不对
异常链:raise ... from 保留"案中案"
真实项目里,一个异常往往是"连锁反应":读文件失败是因为磁盘满,磁盘满又因为写日志撑爆了盘。如果直接 raise MyError("炸了"),原始错误就被丢掉了,排查时一脸懵。异常链就是把"上一个错误"拴在新错误上,让解释器把两层都打印出来。
论两种拴法:隐式 vs 显式
大白话:你在 except 里又 raise,Python 会自动把原异常存进新异常的 __context__(隐式链)。如果你想明确表达"我就是因为这个才报那个错",用 raise 新错 from 原错,它会把链挂到 __cause__,打印时更明确。
raise ... from:把根因一起抛出去
class ConfigError(Exception):
pass
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
# 业务上我们管这叫"配置坏了",但别忘了根因是文件不存在
raise ConfigError(f"配置文件 {path} 无法读取") from e
try:
load_config("nope.ini")
except ConfigError as e:
print("根因:", e.__cause__) # 根因: [Errno 2] No such file...
应用场景:分层架构里特别好用——底层报 数据库连接失败,上层包装成 ServiceUnavailable 返回给调用方,但 from e 让运维在日志里仍能看到"其实是连不上 10.0.0.5:5432"。
防坑:想断开链条用 ... from None
如果你故意不想暴露底层细节(比如对外 API,不想把数据库报错泄露给客户端),用 raise 新错 from None,就能把隐式链掐断,只留下你想让对方看到的那句错误。
练习:异常链小测(点开对答案)
问:在 except 块里只写 raise NewError(),原始异常会丢吗?
答:不会完全丢——Python 会自动放进 __context__,traceback 里仍有 "During handling of the above exception, another exception occurred"。区别是:from e 是显式因果(显示 "The above exception was the direct cause"),from None 才是真的切断。
with 语句:自动收尾
论为什么要 with
打开文件、拿锁、连数据库——这些操作用完必须"收尾"(关文件、放锁)。你忘了关,资源就泄漏。with 语句保证:不管正常结束还是中途报错,收尾一定执行。这叫"上下文管理器"。
# 不用 with:得自己记得 f.close(),中间报错就漏关
f = open("a.txt", "w")
f.write("hi")
f.close()
# 用 with:缩进块结束自动关,哪怕报错
with open("a.txt", "w", encoding="utf-8") as f:
f.write("hi")
# 出了这个缩进,文件已经关好了
自己写上下文管理器
两种写法:用类实现 __enter__/__exit__,或者用 contextlib.contextmanager 装饰器(更简洁)。
from contextlib import contextmanager
import time
@contextmanager
def timer(name):
start = time.perf_counter()
yield # yield 之前是 __enter__,之后是 __exit__
cost = time.perf_counter() - start
print(f"[{name}] 耗时 {cost:.3f}s")
with timer("跑循环"):
for _ in range(100000):
pass
# [跑循环] 耗时 0.008s
类型提示 typing:让代码更稳
from typing import List, Dict, Optional
def greet_all(names: list[str]) -> str:
return "、".join(names)
def find_user(uid: int) -> Optional[dict]:
# 可能返回字典,也可能返回 None
return db.get(uid) if db.exists(uid) else None
# 配合 mypy(pip install mypy)做静态检查
# mypy your_code.py ← 提前揪出类型错误