首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 21 章 内存管理与性能优化

21.1 Python 对象模型与内存布局

Python 中一切皆对象,每个对象至少包含:

import sys

x = 42
print(sys.getsizeof(x))  # 28 字节(CPython 中一个 int 对象)

# 对象的核心组成
# 1. 引用计数(8 字节)
# 2. 类型指针(8 字节)
# 3. 值(大小取决于类型)

# 各类型的基本大小
print(sys.getsizeof(0))         # 28  — int
print(sys.getsizeof(0.0))       # 24  — float
print(sys.getsizeof(True))      # 28  — bool(继承 int)
print(sys.getsizeof(""))        # 49  — 空字符串
print(sys.getsizeof("a"))       # 50  — 1 个 ASCII 字符
print(sys.getsizeof([]))        # 56  — 空列表
print(sys.getsizeof({}))        # 64  — 空字典
print(sys.getsizeof(set()))     # 216 — 空集合
print(sys.getsizeof(()))        # 40  — 空元组

id() 与对象身份

a = [1, 2, 3]
print(id(a))        # 内存地址(CPython 中)
print(hex(id(a)))   # 十六进制地址

# 小整数缓存 (-5 ~ 256)
a = 100
b = 100
print(id(a) == id(b))  # True — 同一个对象

# 字符串驻留(interning)
a = "hello"
b = "hello"
print(a is b)  # True — 短字符串被驻留

21.2 引用计数与垃圾回收

引用计数

Python 主要通过引用计数管理内存:

import sys

a = [1, 2, 3]
print(sys.getrefcount(a))  # 2(a 自身 + getrefcount 的参数)

b = a
print(sys.getrefcount(a))  # 3

c = [a, a]
print(sys.getrefcount(a))  # 5

del b
print(sys.getrefcount(a))  # 4

# 引用计数为 0 时,对象立即被销毁
class Demo:
    def __del__(self):
        print(f"对象被销毁")

d = Demo()
del d  # 立即打印 "对象被销毁"

循环引用问题

# 引用计数无法处理循环引用
a = []
b = []
a.append(b)  # a → b
b.append(a)  # b → a — 循环引用!

del a, b
# 引用计数不为 0(各自被对方引用),但已无法从外部访问
# 需要垃圾回收器处理

分代垃圾回收(Generational GC)

import gc

# 三代:0(新对象)→ 1 → 2(老对象)
# 新对象在第 0 代,如果存活过一次 GC,晋升到第 1 代
print(gc.get_threshold())  # (700, 10, 10)
# 第 0 代:每 700 次分配触发
# 第 1 代:每 10 次第 0 代 GC 触发
# 第 2 代:每 10 次第 1 代 GC 触发

# 手动触发 GC
gc.collect()

# 查看 GC 统计
print(gc.get_stats())

# 查找循环引用
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print(gc.garbage)  # 无法回收的对象

# 禁用 GC(高性能场景,确保无循环引用时)
gc.disable()
# ... 执行代码 ...
gc.enable()

21.3 弱引用 weakref

弱引用不增加引用计数,对象可以被正常回收:

import weakref

class HeavyObject:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f"HeavyObject({self.name!r})"

obj = HeavyObject("data")

# 创建弱引用
ref = weakref.ref(obj)
print(ref())        # HeavyObject('data')

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

# WeakValueDictionary — 值是弱引用的字典
cache = weakref.WeakValueDictionary()

def get_data(key):
    if key in cache:
        return cache[key]
    data = HeavyObject(key)
    cache[key] = data
    return data

d = get_data("test")
print("test" in cache)   # True
del d
print("test" in cache)   # False — 自动清理

# finalize — 注册析构回调
obj = HeavyObject("important")
weakref.finalize(obj, print, "对象被回收了!")
del obj  # 打印 "对象被回收了!"

21.4 __slots__ 与内存优化

import sys

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

class Slotted:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x = x
        self.y = y

r = Regular(1, 2)
s = Slotted(1, 2)

# Regular: 对象大小 + __dict__ 大小
print(sys.getsizeof(r) + sys.getsizeof(r.__dict__))  # ~200 字节
# Slotted: 没有 __dict__
print(sys.getsizeof(s))  # ~56 字节

# 大量实例时效果显著
import tracemalloc
tracemalloc.start()

regulars = [Regular(i, i) for i in range(100_000)]
print(f"Regular: {tracemalloc.get_traced_memory()[0] / 1024 / 1024:.1f} MB")

tracemalloc.stop()
tracemalloc.start()

slotteds = [Slotted(i, i) for i in range(100_000)]
print(f"Slotted: {tracemalloc.get_traced_memory()[0] / 1024 / 1024:.1f} MB")

tracemalloc.stop()

21.5 sys.getsizeof() 与 tracemalloc

sys.getsizeof — 查看单个对象大小

import sys

# 注意:不递归计算引用对象的大小
lst = [1, 2, 3]
print(sys.getsizeof(lst))  # 列表对象本身的大小,不包括元素

# 递归计算总大小
def deep_getsizeof(obj, seen=None):
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    seen.add(obj_id)
    
    size = sys.getsizeof(obj)
    
    if isinstance(obj, dict):
        size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen)
                    for k, v in obj.items())
    elif isinstance(obj, (list, tuple, set, frozenset)):
        size += sum(deep_getsizeof(i, seen) for i in obj)
    
    return size

data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
print(f"浅层: {sys.getsizeof(data)} 字节")
print(f"深层: {deep_getsizeof(data)} 字节")

tracemalloc — 追踪内存分配

import tracemalloc

tracemalloc.start()

# 执行代码
data = [list(range(1000)) for _ in range(1000)]

# 获取当前内存使用
current, peak = tracemalloc.get_traced_memory()
print(f"当前: {current / 1024 / 1024:.1f} MB")
print(f"峰值: {peak / 1024 / 1024:.1f} MB")

# 查看内存分配热点
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

print("\n内存分配 Top 5:")
for stat in top_stats[:5]:
    print(stat)

tracemalloc.stop()

21.6 性能分析

timeit — 微基准测试

import timeit

# 比较两种方式的性能
t1 = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
t2 = timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
print(f"生成器: {t1:.4f}s")
print(f"列表推导: {t2:.4f}s")

# 比较字典查找方式
setup = "d = {i: i*2 for i in range(1000)}"
t1 = timeit.timeit("d.get(500, None)", setup=setup, number=1000000)
t2 = timeit.timeit("500 in d and d[500]", setup=setup, number=1000000)

cProfile — 函数级性能分析

import cProfile

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 分析
cProfile.run("fibonacci(30)")
# 输出每个函数的调用次数、总时间等

# 保存结果到文件
cProfile.run("fibonacci(30)", "profile_output")

# 使用 pstats 分析结果
import pstats
p = pstats.Stats("profile_output")
p.sort_stats("cumulative")
p.print_stats(10)  # 前 10 个最耗时的函数

# 也可以用装饰器形式
def profile(func):
    def wrapper(*args, **kwargs):
        profiler = cProfile.Profile()
        profiler.enable()
        result = func(*args, **kwargs)
        profiler.disable()
        profiler.print_stats(sort="cumulative")
        return result
    return wrapper

21.7 优化技巧

局部变量比全局变量快

import timeit

# 全局变量访问
x = 10
def global_access():
    for _ in range(1000):
        y = x  # LOAD_GLOBAL

# 局部变量访问
def local_access():
    x = 10
    for _ in range(1000):
        y = x  # LOAD_FAST

t1 = timeit.timeit(global_access, number=10000)
t2 = timeit.timeit(local_access, number=10000)
print(f"全局: {t1:.4f}s, 局部: {t2:.4f}s")
# 局部变量通常快 20-30%

缓存

from functools import lru_cache, cache

# lru_cache — 带大小限制的缓存
@lru_cache(maxsize=128)
def expensive_function(n):
    return sum(i * i for i in range(n))

# cache — 无大小限制(Python 3.9+)
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

# 手动缓存(字典)
_cache = {}
def cached_compute(key):
    if key not in _cache:
        _cache[key] = heavy_computation(key)
    return _cache[key]

生成器惰性求值

# 不好 — 一次性加载所有数据到内存
def process_bad(filename):
    lines = open(filename).readlines()  # 全部读入内存
    return [process_line(line) for line in lines]

# 好 — 惰性求值,一行一行处理
def process_good(filename):
    with open(filename) as f:
        for line in f:  # 迭代器,按需读取
            yield process_line(line)

# 使用 sum/max/min 等可以直接接受生成器
total = sum(x * x for x in range(10_000_000))  # 不创建列表

选择正确的数据结构

# 成员检测:set O(1) vs list O(n)
import timeit

data_list = list(range(10000))
data_set = set(range(10000))

t1 = timeit.timeit(lambda: 9999 in data_list, number=10000)
t2 = timeit.timeit(lambda: 9999 in data_set, number=10000)
print(f"list: {t1:.4f}s, set: {t2:.4f}s")
# set 快几百倍

# 头部插入:deque O(1) vs list O(n)
from collections import deque

d = deque()
l = []

t1 = timeit.timeit(lambda: d.appendleft(0), number=100000)
t2 = timeit.timeit(lambda: l.insert(0, 0), number=100000)
print(f"deque: {t1:.4f}s, list: {t2:.4f}s")

字符串拼接

# 不好 — O(n²)
result = ""
for s in string_list:
    result += s

# 好 — O(n)
result = "".join(string_list)

# 好 — 用列表收集再 join
parts = []
for item in data:
    parts.append(transform(item))
result = "".join(parts)

# 更好 — 推导式 + join
result = "".join(transform(item) for item in data)

本章小结:理解 Python 的内存管理(引用计数 + 分代 GC)是写出高性能代码的基础。实用优化策略:用 __slots__ 减少内存、用生成器代替列表、选择正确的数据结构(set vs list)、缓存重复计算。优化前先用 cProfile/timeit 定位瓶颈,不要过早优化。