【第10篇】Python 异常处理与调试入门

写代码这事儿吧,就跟谈恋爱一样------你以为 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 因为 SystemExitKeyboardInterrupt 属于"程序级别的控制信号",你把它们吞了,用户连 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 不存在的模块

踩坑提醒: TypeErrorValueError 是最容易混淆的俩兄弟。简单记法------类型都不对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 是资源清理的"保险丝"------即使 tryreturn 了,finally 也会执行:

python 复制代码
def read_config(path):
    f = open(path)
    try:
        return f.read()
    finally:
        f.close()  # 即使 return 了,这里也会执行

踩坑提醒: 不要在 finally 里写 return!它会覆盖 try 块里的 return 值和异常。这是 Python 最隐蔽的坑之一:

python 复制代码
def 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}")

设计原则:

  1. 自定义异常必须继承 Exception ,不要继承 BaseException
  2. 为你的异常体系设计基类 (如 DownloaderError),这样调用方可以只捕获基类,也能兜住所有子类。
  3. __init__ 里调用 super().__init__(),确保 str(e) 能正常工作。
  4. 异常类名以 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 BA.__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 调试法的终极陷阱------你在 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 系统级灾难,整个程序无法运行

print vs logging 的核心区别:

  • print 写到 stdout,上线后还在,但你控制不了它的开关
  • logging 可以通过 level 参数一秒关闭所有 DEBUG 日志,生产环境只留 INFO 以上
  • logging 自带时间戳、级别、模块名,print 只有你自己拼字符串

日志最佳实践:

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 没反应)和 SystemExitsys.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 作为基类,InvalidURLErrorRetryExhaustedError 等继承它
异常携带上下文 RetryExhaustedError 携带 URL、重试次数、最后错误
异常链 raise RetryExhaustedError(...) from e 保留原始异常
精确捕获 分别 except HTTPErrorexcept URLErrorexcept 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 写错 ValueErrorAttributeError InvalidURLError,清楚提示原因
4xx 错误 HTTPError,调用方不知道该不该重试 4xx 不重试,5xx 重试,逻辑清晰
排查问题 没有 log,两眼一黑 完整日志,精确到每次尝试

这就是异常处理的价值------它不会让你的程序不出错,但它会让每一次出错都可控、可查、可恢复。


九、总结

主题 核心要点
异常层次 一切继承 Exception,别碰 BaseException
try 四段式 else 放正常逻辑,finally 放资源清理
自定义异常 继承 Exception,设计基类,携带上下文
异常链 raise from e 保留因果,raise from None 切断
调试 breakpoint() > printlogging > print(生产环境)
反模式 except、吞异常、控制流用异常、finallyreturn
相关推荐
安逸sgr1 小时前
神经网络是怎么工作的?从神经元到多层感知机
人工智能·ai·大模型·agent·智能体
宝桥南山1 小时前
Microsoft Agent Framework(.NET) - 尝试一下从MCP Server上获取Agent Skills
ai·微软·c#·aigc·.net·.netcore
FIT2CLOUD飞致云1 小时前
学习笔记丨MaxKB原生工作流实现AI PPT生成
人工智能·ai·开源·智能体·maxkb
程序员雷欧1 小时前
Java 反射深度解析:从原理到源码的全面剖析
java·开发语言·python
meilindehuzi_a1 小时前
Harness 工程详解:用 Best of N Sampling 与 LLM as Judge 构建生成、评测、择优闭环
ai
我的xiaodoujiao2 小时前
快速学习Python基础知识详细图文教程18--多线程
开发语言·python·学习·测试工具
MicrosoftReactor2 小时前
技术速递|GitHub Copilot App 中的堆叠会话与拉取请求
ai·github·copilot·agent
GlobalInfo2 小时前
AI与另类数据融合,市场研究正在从“经验驱动”走向“数据智能”
大数据·网络·人工智能·ai