《Effective Python》读书笔记09: 稳定性与性能

作者: andylin02
学习章节: 第8章 - 稳定性与性能
关键词: 异常处理, 资源管理, with, 上下文管理器, 性能分析, 内存优化, slots, 弱引用, 字符串驻留


第8章思维导图:9条建议全景

第8章 稳定性与性能 9条建议

错误处理

第60条: 合理利用异常

第61条: 异常链与raise from

第62条: 资源管理与with

调试与配置

第63条: 使用pdb调试(前导)

第64条: 用tracemalloc分析内存

性能与优化

第65条: 使用timeit和cProfile

第66条: 了解字符串驻留

第67条: 用__slots__节省内存

第68条: 弱引用防止内存泄漏


第8章详细笔记:逐条精讲与源代码

本章聚焦如何写出长期稳定运行性能优异的 Python 程序。从异常管理到内存优化,每一步都让代码更健壮。

第60条:用 raise 合理利用异常

核心:异常是 Python 处理错误的推荐方式。不要因为过去习惯返回错误码而回避异常。合理使用 try/except 可将错误处理与正常逻辑分离,提高可读性。

# 不推荐:返回错误码让调用方检查
def divide(a, b):
    if b == 0:
        return None, "division by zero"
    return a / b, None

# 推荐:抛出异常,让调用方决定如何处理
def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"出错: {e}")

异常层次结构

  • BaseException
    • SystemExit
    • KeyboardInterrupt
    • Exception
      • ValueError
      • TypeError
      • IOError

关键收获:明确区分正常与异常代码路径,让函数签名只返回成功结果。


第61条:使用 raise from 表达异常链

核心:当在 except 块中抛出另一个异常时,用 raise ... from ... 可以将原始异常作为上下文保留,方便调试时追踪根本原因。

def load_config(filepath):
    try:
        with open(filepath) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ValueError(f"配置文件缺失: {filepath}") from e

try:
    load_config("missing.json")
except ValueError as e:
    print(e)           # 配置文件缺失: missing.json
    print(e.__cause__) # FileNotFoundError

若不使用 from:新的异常会丢失原始上下文,使得根因难以定位。使用 raise ... from None 则是有意隐藏上下文。

关键收获raise ... from ... 让异常形成清晰的因果链,是大型系统排错的关键。


第62条:使用 with 语句管理资源和上下文

核心with 语句确保无论是否发生异常,资源(文件、锁、网络连接等)都能被正确释放。这远比手写 try/finally 简洁安全。

# 推荐:with 自动关闭文件
with open("data.txt", "r") as f:
    content = f.read()

# 自定义上下文管理器:实现 __enter__ 和 __exit__
class ManagedFile:
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        self.file = open(self.name, 'w')
        return self.file
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()

with ManagedFile('hello.txt') as f:
    f.write('hello, world!')

__exit__ 参数:三个参数分别代表异常类型、值和回溯。若返回 True,则压制异常。

关键收获:将资源获取和释放封装在 with 中,是避免资源泄漏的最简单方式。


第63条:使用 tracemalloc 追踪内存使用

核心:Python 3.4+ 的 tracemalloc 模块可以跟踪内存分配,显示哪些代码段占用内存最多,是定位内存泄漏和性能瓶颈的利器。

import tracemalloc

tracemalloc.start()

# 模拟内存分配
data = [b'x' * 1024 for _ in range(10000)]  # 约10MB

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("Top 10 内存分配位置:")
for stat in top_stats[:10]:
    print(stat)

关键收获tracemalloc 帮助定位意料之外的大对象,是内存调优的第一步。


第64条:使用 timeitcProfile 分析性能

核心:性能优化前必须先测量timeit 精确测量微小代码块执行时间;cProfile 定位整个程序的性能瓶颈。

# timeit 示例
import timeit

# 比较列表推导与 map 的性能
t1 = timeit.timeit('[x**2 for x in range(1000)]', number=10000)
t2 = timeit.timeit('list(map(lambda x: x**2, range(1000)))', number=10000)
print(f"列表推导: {t1:.3f}s, map: {t2:.3f}s")

# cProfile 命令行用法
# python -m cProfile -s cumtime my_script.py

关键收获:不要靠猜测优化,先让数据说话。cProfile 按累计时间排序,看得最清楚。


第65条:使用 __slots__ 减少内存开销

核心:类的每个实例默认用 __dict__ 存储属性,而这个字典是内存大户。通过在类中定义 __slots__,可固定属性集合,移除 __dict__,极大减少每个实例的内存占用。

class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedPoint:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys
rp = RegularPoint(1, 2)
sp = SlottedPoint(1, 2)
print(sys.getsizeof(rp.__dict__))  # 约 104 字节(仅 dict)
print(sys.getsizeof(sp))           # 约 48 字节

代价

  • 不能再随意添加新属性(除非在 __slots__ 中加入 __dict__
  • 不支持多继承中的某些组合

关键收获:当要创建大量(百万级)实例时,__slots__ 是必备优化手段。


第66条:了解字符串驻留与 intern

核心:Python 对部分字符串自动驻留(intern),使得相同值的字符串共享同一内存。在代码热路径上,手动调用 sys.intern 可以大幅减少内存并加速字典查找。

import sys

a = "hello world"
b = "hello world"
print(a is b)  # 可能为 True(自动驻留)

# 对运行时生成的字符串,手动驻留
c = sys.intern("some long string" * 100)
d = sys.intern("some long string" * 100)
print(c is d)  # True

关键收获:在大量字符串比较或作为字典键的场景,intern 能显著提升效率。


第67条:用弱引用防止内存泄漏

核心:强引用(普通引用)使对象不被回收。当某个对象仅需被观察而不应阻止其死亡时,使用 weakref 弱引用。常用于缓存和观察者模式。

import weakref

class ExpensiveObject:
    pass

obj = ExpensiveObject()
ref = weakref.ref(obj)  # 弱引用

print(ref())  # 返回 obj
del obj
print(ref())  # None,对象已被回收

# WeakValueDictionary 实现自动清空缓存
cache = weakref.WeakValueDictionary()
obj2 = ExpensiveObject()
cache['key'] = obj2
del obj2
print(list(cache.keys()))  # 空

关键收获:弱引用让你从“拥有”变为“知道”,打破循环引用,防止内存泄漏。


第68条:正确选择和实现容器

核心:本章(第8章)中关于容器优化的建议:字典和集合的查找是 O(1),远比列表的 O(n) 快。但容器本身也有开销,应根据场景选择合适的结构。

# 需要快速成员测试时用集合
valid_ids = {'A1', 'B2', 'C3'}
if 'A1' in valid_ids:  # O(1)
    print("找到")

# 需要频繁两端添加删除时用 collections.deque
from collections import deque
queue = deque()
queue.append(1)
queue.appendleft(0)
print(queue.popleft())  # 0

关键收获:选对容器,性能提升往往立竿见影,而不必过早陷入微优化。


性能分析与优化决策流程

CPU密集型计算

内存占用高

I/O 等待

程序性能不符合预期

使用cProfile定位瓶颈

瓶颈类型?

优化算法/数据结构

使用tracemalloc分析

使用__slots__减少实例内存

考虑弱引用打破循环

引入异步或缓存

使用timeit微基准测试验证

改进达标?

完成


第8章总结:稳定与高效的平衡艺术

  1. 异常即控制流:用 raise 抛出异常,用 raise from 保留上下文,让错误可见、可追踪。
  2. 资源需守护:所有需要关闭的资源都用 with 包裹,无论是否发生异常。
  3. 先测量,再优化timeit 测小,cProfile 测大,tracemalloc 测内存,数据驱动决策。
  4. 内存即成本__slots__ 省实例字典,weakref 破循环引用,intern 省重复字符串。
  5. 容器即速度:集合 O(1) 查找,deque 快速两端操作,选择与需求匹配的结构。

掌握这些原则,你的代码将能长期稳定运行,并在需要时实现精准的性能突破。


下一章预告:第9章 - 测试与调试

在保障稳定性与性能之后,我们进入质量保障的最后一道防线:测试与调试

你将学到:

  • 如何编写有效的单元测试(使用 unittestpytest
  • Mock 依赖,隔离测试目标
  • 使用 pdb 进行交互式调试的艺术
  • 复杂场景下的测试策略

第9章将帮助你构建可靠的测试体系,自信发布每一行代码!


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

andylin02

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值