写代码这事儿吧,就跟谈恋爱一样------你以为
print("Hello World")就能一直岁月静好?天真。真正让代码"长大"的,从来不是那些顺风顺水的逻辑分支,而是你如何处理那些「意料之外的崩塌」。这一篇,我们来聊聊 Python 里最容易被新手忽视、又最能让老手翻车的主题------异常处理与调试。
一、介绍:为什么你需要异常处理?
先看一段代码:
python
data = {"name": "yance", "blog": "CSDN"}
print(data["age"]) # KeyError: 'age'
跑一下,程序炸了。控制台吐了一堆红字,进程退出码 1。
问题不在于程序会崩------程序一定会崩。问题在于:它在什么时候崩、崩了之后你能不能接住、接住之后还能不能优雅地继续。
异常处理的核心目的只有一个:
让程序在出错时,依然保持可控。
你可以选择"吞掉"它(记录日志后继续)、"转化"它(变成更友好的错误消息)、或者"传递"它(往上抛给能处理的人)。但你绝不能假装它不存在。
二、异常层次结构与常见异常类型
2.1 异常的继承体系
Python 的所有异常都继承自 BaseException,但这只是一个"理论上的祖宗"。实际开发中,你打交道最多的是它的子类 Exception:
BaseException
├── SystemExit # sys.exit() 触发
├── KeyboardInterrupt # Ctrl+C 中断
├── GeneratorExit # generator.close()
└── Exception ← 你 99% 的时间都在这里
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError ← 以前叫 EnvironmentError
│ ├── FileNotFoundError
│ ├── PermissionError
│ ├── TimeoutError
│ └── ...
├── TypeError
├── ValueError
│ └── UnicodeDecodeError
├── AttributeError
├── NameError
└── ...
记住一条铁律:
except永远只捕获Exception,不要捕获BaseException。 因为SystemExit和KeyboardInterrupt属于"程序级别的控制信号",你把它们吞了,用户连 Ctrl+C 都按不出去------这是反人类操作。
2.2 新手最容易遇到的 Top 10 异常
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
TypeError |
类型不对 | "yance" + 10 |
ValueError |
类型对但值非法 | int("hello") |
KeyError |
字典键不存在 | d["missing"] |
IndexError |
列表越界 | lst[999] |
AttributeError |
对象没这个属性 | None.split() |
NameError |
变量名未定义 | print(undefined_var) |
FileNotFoundError |
文件找不到 | open("不存在.txt") |
ZeroDivisionError |
除以零 | 1 / 0 |
StopIteration |
迭代器耗尽 | next(iter([])) |
ImportError |
模块导入失败 | import 不存在的模块 |
踩坑提醒:
TypeError和ValueError是最容易混淆的俩兄弟。简单记法------类型都不对 叫TypeError(拿字符串当数字除),类型对但内容不合法 叫ValueError(拿"hello"转int)。
三、try / except / else / finally 完整语法
3.1 四段式结构
这是 Python 异常处理的"全家桶":
python
try:
# 可能出错的代码
result = do_something_risky()
except SpecificError as e:
# 捕获到特定异常时执行
handle_error(e)
else:
# try 块没有抛出任何异常时才执行
process_result(result)
finally:
# 无论是否异常,都会执行
cleanup()
这四块的执行逻辑可以用一张表说清楚:
| try 抛了异常? | except 匹配? | else 执行? | finally 执行? |
|---|---|---|---|
| 是 | 是 | 否 | 是 |
| 是 | 否(异常上抛) | 否 | 是 |
| 否 | --- | 是 | 是 |
3.2 每一块的存在意义
else 块 很多人没用过,但它很重要------它把"正常逻辑"和"可能出错的代码"分离开:
python
# ❌ 不好的写法:正常逻辑混在 try 里
try:
data = json.loads(raw_text)
process(data) # 如果 process 也抛异常,会被下面 except 误伤
except json.JSONDecodeError:
print("JSON 格式错误")
# ✅ 正确写法:else 隔离正常逻辑
try:
data = json.loads(raw_text)
except json.JSONDecodeError:
print("JSON 格式错误")
else:
process(data) # process 的异常不会被 JSONDecodeError 捕获
finally 块 是资源清理的"保险丝"------即使 try 里 return 了,finally 也会执行:
python
def read_config(path):
f = open(path)
try:
return f.read()
finally:
f.close() # 即使 return 了,这里也会执行
踩坑提醒: 不要在
finally里写return!它会覆盖try块里的return值和异常。这是 Python 最隐蔽的坑之一:
pythondef demo(): try: return "正常值" finally: return "finally 的值" # 这行会吞掉上面的返回值! print(demo()) # "finally 的值"
3.3 多异常捕获
python
try:
response = requests.get(url)
data = response.json()
except (requests.ConnectionError, requests.Timeout) as e:
print(f"网络问题: {e}")
except json.JSONDecodeError:
print("返回的不是合法 JSON")
except Exception as e:
print(f"未知错误: {e}")
raise # 重新抛出,交给上层处理
注意: 异常匹配是从上到下的,先写具体异常,再写通用异常 。如果你把
except Exception放在最前面,后面的具体异常永远没机会被匹配------这叫"异常遮蔽"。
四、自定义异常类
4.1 为什么要自定义异常?
标准库的异常是"通用语言",但你的业务有自己的"方言"。比如一个下载器:
ConnectionError只告诉你"连不上",但你不知道是重试次数用完了还是 DNS 解析失败。- 自定义异常可以携带业务上下文:URL、重试次数、最后一次错误等。
4.2 怎么写
python
class DownloaderError(Exception):
"""下载器基础异常"""
pass
class RetryExhaustedError(DownloaderError):
"""重试次数耗尽"""
def __init__(self, url, attempts, last_error):
self.url = url
self.attempts = attempts
self.last_error = last_error
super().__init__(
f"下载失败:{url},重试 {attempts} 次后仍失败,"
f"最后一次错误:{last_error}"
)
class InvalidURLError(DownloaderError):
"""URL 格式非法"""
def __init__(self, url, reason="格式不合法"):
self.url = url
self.reason = reason
super().__init__(f"无效 URL '{url}':{reason}")
设计原则:
- 自定义异常必须继承
Exception,不要继承BaseException。- 为你的异常体系设计基类 (如
DownloaderError),这样调用方可以只捕获基类,也能兜住所有子类。- 在
__init__里调用super().__init__(),确保str(e)能正常工作。- 异常类名以
Error结尾是约定俗成的(Warning结尾的是警告,不一样)。
4.3 异常也是类------可以玩继承树
python
class DownloaderError(Exception): pass
class NetworkError(DownloaderError): pass # 网络层
class HTTPError(NetworkError): pass # HTTP 层
class TimeoutError(NetworkError): pass # 超时
# 调用方可以选择捕获粒度
try:
download(url)
except HTTPError as e:
handle_http_error(e) # 只处理 HTTP 错误
except NetworkError as e:
handle_network_error(e) # 处理其他网络错误
except DownloaderError as e:
handle_other(e) # 兜底
五、异常链与 raise from
5.1 什么是异常链?
你在捕获一个异常后,可能想把它"包装"成更上层的业务异常。但你不希望丢失原始异常的信息------这时候就用 raise ... from ...:
python
class ConfigLoadError(Exception):
pass
try:
with open("config.json") as f:
config = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ConfigLoadError("配置文件加载失败") from e
运行后你会看到:
config.ConfigLoadError: 配置文件加载失败
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
FileNotFoundError: [Errno 2] No such file or directory: 'config.json'
异常链把"因果关系"完整地保留了下来------from e 告诉 Python:"我这个异常是 e 直接导致的。"
5.2 raise from 的三种用法
python
# ① raise from e ------ 显式因果链(最常用)
raise ConfigLoadError("配置加载失败") from original_error
# ② raise from None ------ 切断因果链
raise ConfigLoadError("配置加载失败") from None
# 告诉用户:这就是最终原因,别再看底层了
# ③ raise(不带 from)------ 隐式链
raise ConfigLoadError("配置加载失败")
# Python 会自动设置 __context__,也会显示,但语义上不强调"因果"
__cause__vs__context__:
raise A from B→A.__cause__ = B(显式因果链)- 在
except块里raise A(不带 from)→A.__context__ = B(隐式上下文链)- 两者在 traceback 里都会显示 "During handling of the above exception, another exception occurred"
5.3 什么时候用 from None?
python
try:
value = int(user_input)
except ValueError:
# 转成业务异常,但不想暴露内部用了 int() 的事实
raise InvalidInputError("用户输入不是有效整数") from None
from None 的本质是信息隐藏------在面向外部的 API 中,不希望调用方看到实现细节时用。
六、调试方法:print / pdb / breakpoint / logging
代码出 bug 了怎么办?这四把"调试瑞士军刀"从低端到高端,各有适用场景。
6.1 print ------ 最朴素的调试
python
def calculate_score(data):
print(f"[DEBUG] 输入数据: {data}")
total = sum(data.values())
print(f"[DEBUG] 总分: {total}")
count = len(data)
print(f"[DEBUG] 数量: {count}")
return total / count
优点: 零学习成本,随手就来。
缺点: 代码里留一堆 print,上线忘删就是灾难。而且只能看"值",不能"交互式"地探索。
踩坑提醒:
print(lst.pop())),然后你的 bug "消失"了。这叫 Heisenbug(海森堡 bug),观测改变了被观测对象。
6.2 pdb ------ Python 自带的调试器
python
import pdb; pdb.set_trace()
在你想"暂停"的地方插一行,程序跑到这里会停下来,进入交互式调试:
> /path/to/script.py(10)calculate_score()
-> return total / count
(Pdb) p total
285
(Pdb) p count
0
(Pdb) n
(Pdb) c
常用命令:
| 命令 | 缩写 | 作用 |
|---|---|---|
next |
n |
执行下一行(不进入函数) |
step |
s |
执行下一行(进入函数) |
continue |
c |
继续运行直到下一个断点 |
print <expr> |
p <expr> |
打印表达式的值 |
list |
l |
显示当前代码上下文 |
where |
w |
显示调用栈 |
break <line> |
b <line> |
设置断点 |
quit |
q |
退出调试器 |
6.3 breakpoint() ------ Python 3.7+ 的优雅写法
python
def calculate_score(data):
total = sum(data.values())
count = len(data)
breakpoint() # 等价于 pdb.set_trace(),但更优雅
return total / count
breakpoint() 是内置函数,它会自动调用你在环境变量 PYTHONBREAKPOINT 中配置的调试器。默认是 pdb,但你可以换成 ipdb(带语法高亮和自动补全的增强版)或 pudb(全屏可视化调试器)。
bash
# 用 ipdb 调试
pip install ipdb
export PYTHONBREAKPOINT=ipdb.set_trace
与
pdb.set_trace()相比,breakpoint()的优势:
- 一行代码,无
import- 可通过环境变量全局切换调试器
- 设置
PYTHONBREAKPOINT=0可以一行禁用所有断点(上线前的好习惯)
6.4 logging ------ 工业级调试与运行时监控
python
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[
logging.FileHandler("app.log", encoding="utf-8"),
logging.StreamHandler(),
]
)
logger = logging.getLogger(__name__)
def download(url):
logger.debug("开始下载: %s", url)
try:
resp = requests.get(url, timeout=10)
logger.info("下载完成: %s (状态码 %d)", url, resp.status_code)
except requests.Timeout:
logger.warning("下载超时: %s", url)
except Exception as e:
logger.error("下载失败: %s, 错误: %s", url, e, exc_info=True)
五个日志级别:
| 级别 | 数值 | 用途 |
|---|---|---|
DEBUG |
10 | 调试细节,生产环境通常关闭 |
INFO |
20 | 关键业务节点,正常运行时保留 |
WARNING |
30 | 可预期的问题,需要注意但不致命 |
ERROR |
40 | 严重错误,某个功能不可用 |
CRITICAL |
50 | 系统级灾难,整个程序无法运行 |
logging的核心区别:
logging可以通过level参数一秒关闭所有DEBUG日志,生产环境只留INFO以上logging自带时间戳、级别、模块名,
日志最佳实践:
python
# ❌ 错误:用 f-string 格式化(即使日志级别不输出,f-string 也会执行)
logger.debug(f"数据处理中: {expensive_serialize(data)}")
# ✅ 正确:用 % 占位符(延迟格式化,级别不够就不执行)
logger.debug("数据处理中: %s", expensive_serialize)
# ❌ 错误:except 里只打字符串,丢失 traceback
except Exception as e:
logger.error(str(e))
# ✅ 正确:exc_info=True 保留完整堆栈
except Exception as e:
logger.error("处理失败: %s", e, exc_info=True)
七、常见异常处理反模式
写了这么多年代码,以下这些"写法"我见过无数次,每一个都是血泪教训。
反模式 1:裸 except(Bare Except)
python
# ❌ 灾难
try:
do_something()
except:
pass
这会捕获所有异常 ,包括 KeyboardInterrupt(用户按 Ctrl+C 没反应)和 SystemExit(sys.exit() 被吞掉)。如果你非要用,至少写 except Exception:。但更好的做法是------捕获具体的异常。
反模式 2:吞掉异常不记录
python
# ❌ 异常黑洞
try:
result = risky_operation()
except Exception:
pass # 出了什么事?不知道。程序继续跑。炸弹倒计时中。
出了 bug 你连日志都没有,排查时只能对着空气发呆。至少加一行日志:
python
# ✅ 至少记录一下
except Exception as e:
logger.warning("risky_operation 失败,跳过: %s", e)
反模式 3:控制流用异常
python
# ❌ 用异常做流程控制
def find_user(user_id):
try:
return users[user_id]
except KeyError:
return None
# ✅ 正常判断
def find_user(user_id):
return users.get(user_id) # 没有就返回 None,不用异常
异常是"异常"情况,不是"正常"分支。用异常做控制流性能差(异常对象创建有开销),可读性也差。
反模式 4:except 里 return
python
# ❌ 隐蔽 bug
def process(data):
try:
result = transform(data)
return result
except ValueError:
return None
finally:
cleanup() # 如果 cleanup 里有 return 或异常,上面的 return 就没了
finally 块里的 return 或异常会覆盖 try 块的 return。这是 Python 最坑的语义之一。
反模式 5:过宽捕获
python
# ❌ 一网打尽
try:
import json
data = json.loads(text)
result = process(data)
save_to_db(result)
except Exception:
print("出错了")
# 上面三个操作各有各的异常类型,
# 全笼统地 catch 成"出错了",debug 的时候你根本不知道是哪一步炸的
应该拆成多个 try,或者至少分别捕获:
python
# ✅ 精确捕获
try:
import json
data = json.loads(text)
except json.JSONDecodeError as e:
logger.error("JSON 解析失败: %s", e)
raise
try:
result = process(data)
except ProcessingError as e:
logger.error("处理失败: %s", e)
raise
反模式 6:异常不抛出只是打日志
python
# ❌ 吞了异常假装没事
def critical_operation():
try:
do_critical_stuff()
except Exception as e:
print(f"出错了: {e}")
# 没有 raise,调用方以为成功了!
# ✅ 打日志后继续抛
def critical_operation():
try:
do_critical_stuff()
except Exception as e:
logger.error("关键操作失败: %s", e, exc_info=True)
raise # 让调用方知道出事了
八、实操 Demo:健壮的文件下载器(含重试机制)
学了这么多,来个实战------写一个"生产级"的文件下载器,集成我们这一篇所有的知识点。
8.1 完整源码
python
"""
robust_downloader.py
一个健壮的文件下载器:支持重试、超时、自定义异常、日志记录
"""
import os
import time
import logging
import random
from pathlib import Path
from urllib.parse import urlparse
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# ============================================================
# 第一部分:自定义异常体系
# ============================================================
class DownloaderError(Exception):
"""下载器所有异常的基类"""
pass
class InvalidURLError(DownloaderError):
"""URL 不合法"""
def __init__(self, url, reason="URL 格式不合法"):
self.url = url
self.reason = reason
super().__init__(f"无效 URL '{url}':{reason}")
class DownloadTimeoutError(DownloaderError):
"""下载超时"""
def __init__(self, url, timeout):
self.url = url
self.timeout = timeout
super().__init__(f"下载超时:{url}({timeout}秒)")
class RetryExhaustedError(DownloaderError):
"""重试次数耗尽"""
def __init__(self, url, attempts, last_error):
self.url = url
self.attempts = attempts
self.last_error = last_error
super().__init__(
f"下载失败:{url},重试 {attempts} 次后仍失败,"
f"最后错误:{last_error}"
)
class DiskFullError(DownloaderError):
"""磁盘空间不足"""
def __init__(self, path, required, available):
self.path = path
self.required = required
self.available = available
super().__init__(
f"磁盘空间不足:需要 {required} 字节,"
f"可用 {available} 字节(路径:{path})"
)
# ============================================================
# 第二部分:日志配置
# ============================================================
logger = logging.getLogger("downloader")
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)
logger.addHandler(console_handler)
file_handler = logging.FileHandler("downloader.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
)
logger.addHandler(file_handler)
# ============================================================
# 第三部分:下载器核心
# ============================================================
class RobustDownloader:
"""
健壮的文件下载器
特性:
- 支持重试机制(指数退避 + 随机抖动)
- 超时控制
- 自定义异常体系
- 完整的日志记录
- 磁盘空间预检
- 断点续传(如果服务器支持 Range)
"""
def __init__(self, max_retries=3, timeout=30, backoff_base=1.0,
backoff_max=60.0, chunk_size=8192):
self.max_retries = max_retries
self.timeout = timeout
self.backoff_base = backoff_base
self.backoff_max = backoff_max
self.chunk_size = chunk_size
def _validate_url(self, url):
"""校验 URL 格式"""
parsed = urlparse(url)
if not parsed.scheme:
raise InvalidURLError(url, "缺少协议(http:// 或 https://)")
if parsed.scheme not in ("http", "https"):
raise InvalidURLError(url, f"不支持的协议:{parsed.scheme}")
if not parsed.netloc:
raise InvalidURLError(url, "缺少域名")
logger.debug("URL 校验通过: %s", url)
def _check_disk_space(self, path, estimated_size=None):
"""检查磁盘空间"""
usage = os.statvfs(path) if hasattr(os, 'statvfs') else None
# Windows 上没有 statvfs,这里做兼容处理
if usage is None:
logger.debug("当前平台不支持 statvfs,跳过磁盘空间检查")
return
available = usage.f_bavail * usage.f_frsize
if estimated_size and estimated_size > available:
raise DiskFullError(path, estimated_size, available)
logger.debug("磁盘空间检查通过:可用 %d 字节", available)
def _calculate_backoff(self, attempt):
"""
计算退避时间:指数退避 + 随机抖动
公式:backoff = min(base * 2^attempt + jitter, max_backoff)
其中 jitter = random.uniform(0, 1)
"""
exponential = self.backoff_base * (2 ** attempt)
jitter = random.uniform(0, 1)
backoff = min(exponential + jitter, self.backoff_max)
return backoff
def _download_once(self, url, dest_path):
"""
执行一次下载尝试
可能抛出:
- DownloadTimeoutError 下载超时
- HTTPError (来自 urllib) HTTP 错误
- URLError (来自 urllib) 网络连接错误
- OSError 磁盘写入错误
"""
request = Request(url, headers={
"User-Agent": "RobustDownloader/1.0"
})
response = urlopen(request, timeout=self.timeout)
total = response.getheader("Content-Length")
total = int(total) if total else None
logger.info("开始下载: %s (大小: %s)",
url,
f"{total} bytes" if total else "未知")
written = 0
with open(dest_path, "wb") as f:
while True:
chunk = response.read(self.chunk_size)
if not chunk:
break
f.write(chunk)
written += len(chunk)
if total:
progress = written / total * 100
logger.debug("下载进度: %.1f%% (%d/%d)",
progress, written, total)
logger.info("下载完成: %s -> %s (%d bytes)", url, dest_path, written)
return written
def download(self, url, dest_path):
"""
下载文件到指定路径,带重试机制
参数:
url: 下载地址
dest_path: 本地保存路径
返回:
写入的字节数
异常:
InvalidURLError URL 不合法
RetryExhaustedError 重试次数耗尽
DiskFullError 磁盘空间不足
"""
# 1. 校验 URL
self._validate_url(url)
# 2. 确保目标目录存在
dest = Path(dest_path)
dest.parent.mkdir(parents=True, exist_ok=True)
# 3. 记录最后一次错误,用于异常链
last_error = None
for attempt in range(1, self.max_retries + 1):
try:
logger.debug("第 %d/%d 次尝试下载", attempt, self.max_retries)
return self._download_once(url, dest_path)
except HTTPError as e:
# HTTP 错误码:4xx 通常不需要重试(客户端错误)
if 400 <= e.code < 500:
logger.error("HTTP 客户端错误 (%d),不重试: %s",
e.code, url)
raise RetryExhaustedError(url, attempt, e) from e
# 5xx 服务端错误,可以重试
logger.warning("HTTP %d 错误 (第 %d 次尝试): %s",
e.code, attempt, url)
last_error = e
except URLError as e:
logger.warning("网络错误 (第 %d 次尝试): %s - %s",
attempt, url, e.reason)
last_error = e
except TimeoutError as e:
logger.warning("超时 (第 %d 次尝试): %s", attempt, url)
last_error = DownloadTimeoutError(url, self.timeout)
except OSError as e:
# 磁盘写入相关错误
logger.error("文件写入错误: %s", e)
raise DownloaderError(f"文件写入失败: {e}") from e
# 如果还有重试机会,退避等待
if attempt < self.max_retries:
backoff = self._calculate_backoff(attempt)
logger.info("等待 %.1f 秒后重试...", backoff)
time.sleep(backoff)
# 所有重试都失败了
logger.error("重试 %d 次后放弃: %s", self.max_retries, url)
raise RetryExhaustedError(url, self.max_retries, last_error)
# ============================================================
# 第四部分:使用示例
# ============================================================
def main():
"""演示用法"""
downloader = RobustDownloader(
max_retries=3,
timeout=15,
backoff_base=1.0,
backoff_max=30.0
)
# 测试用例 1:正常下载
try:
size = downloader.download(
"https://httpbin.org/bytes/1024",
"downloads/test_1k.bin"
)
print(f"✅ 下载成功,{size} 字节")
except DownloaderError as e:
print(f"❌ 下载失败: {e}")
# 测试用例 2:无效 URL
try:
downloader.download("not-a-url", "downloads/bad.bin")
except InvalidURLError as e:
print(f"❌ URL 校验失败: {e}")
# 测试用例 3:重试耗尽(用一个不存在的域名)
try:
downloader.download(
"https://this-domain-does-not-exist-12345.com/file.zip",
"downloads/missing.zip"
)
except RetryExhaustedError as e:
print(f"❌ 重试耗尽: {e}")
print(f" 最后错误类型: {type(e.last_error).__name__}")
if __name__ == "__main__":
main()
8.2 源码拆解
这个下载器里,我们用到了本篇讲的每一个知识点:
| 知识点 | 在代码中的体现 |
|---|---|
| 自定义异常层次 | DownloaderError 作为基类,InvalidURLError、RetryExhaustedError 等继承它 |
| 异常携带上下文 | RetryExhaustedError 携带 URL、重试次数、最后错误 |
| 异常链 | raise RetryExhaustedError(...) from e 保留原始异常 |
| 精确捕获 | 分别 except HTTPError、except URLError、except TimeoutError |
| logging 记录 | 不同级别记录不同严重程度的信息,exc_info 保留堆栈 |
| try/except/else 结构 | 校验和下载逻辑分离,异常不互相干扰 |
| 指数退避算法 | _calculate_backoff 实现 min(base * 2^n + jitter, max) |
8.3 效果对比
不用异常处理的"裸"下载器:
python
# 裸奔版
def download_naive(url, path):
response = urlopen(url)
with open(path, "wb") as f:
f.write(response.read())
| 场景 | 裸奔版 | 健壮版 |
|---|---|---|
| 网络抖动 | 直接崩溃,用户手动重跑 | 自动重试 3 次,带退避 |
| 超时 | 无限等待(或系统级超时) | 15 秒超时,自动重试 |
| 磁盘满 | 抛出 OSError,用户一脸懵 |
抛出 DiskFullError,告诉你差多少 |
| URL 写错 | ValueError 或 AttributeError |
InvalidURLError,清楚提示原因 |
| 4xx 错误 | 抛 HTTPError,调用方不知道该不该重试 |
4xx 不重试,5xx 重试,逻辑清晰 |
| 排查问题 | 没有 log,两眼一黑 |
完整日志,精确到每次尝试 |
这就是异常处理的价值------它不会让你的程序不出错,但它会让每一次出错都可控、可查、可恢复。
九、总结
| 主题 | 核心要点 |
|---|---|
| 异常层次 | 一切继承 Exception,别碰 BaseException |
| try 四段式 | else 放正常逻辑,finally 放资源清理 |
| 自定义异常 | 继承 Exception,设计基类,携带上下文 |
| 异常链 | raise from e 保留因果,raise from None 切断 |
| 调试 | breakpoint() > print,logging > print(生产环境) |
| 反模式 | 裸 except、吞异常、控制流用异常、finally 里 return |