今天周四不能早下班儿,博主准备摸个鱼~
我们先从 Python 的内存管理机制和字节码执行原理这两个核心底层知识开始。这部分是理解 Python 运行机制的基础,也是区分 “会用” 和 “懂原理” 的关键。
一、内存管理机制:Python 如何管理对象的生命周期
Python 的内存管理主要依赖三大机制:引用计数(核心)、垃圾回收(辅助)、内存池(优化)。我们逐个拆解并通过代码验证。
1. 引用计数:对象存活的核心指标
每个 Python 对象都有一个 “引用计数器”,记录当前有多少个变量引用它。当计数器归 0 时,对象会被立即销毁,释放内存。
验证代码(用sys.getrefcount()
查看引用数):
import sys
# 创建一个对象,如果打印的是10这种Python 中预缓存的小整数,它的引用计数会比普通对象高很多。
a = "鸿泥雪瓜"
print(sys.getrefcount(a)) # 输出:4(变量a的引用 + getrefcount参数的临时引用 + 字符串创建时的 “常量池引用”+ 解释器的 “临时管理引用”)
# 增加引用
b = a
print(sys.getrefcount(a)) # 输出:5 (a、b、getrefcount参数、常量池、临时管理)
# 减少引用(删除变量)
del b
print(sys.getrefcount(a)) # 输出:4 (a、getrefcount参数、常量池、临时管理)
# 再次减少引用
del a
# 此时对象10的引用计数归0,内存被释放(无法再通过变量访问)
引用计数增加的场景:
- 变量赋值(
x = obj
) - 作为参数传递(
func(obj)
) - 放入容器(
list.append(obj)
)
引用计数减少的场景:
- 变量被删除(
del x
) - 变量重新赋值(
x = other_obj
) - 离开作用域(如函数执行结束,局部变量被销毁)
- 从容器中移除(
list.remove(obj)
)
2. 垃圾回收:解决引用计数搞不定的问题
引用计数有个致命缺陷:无法处理循环引用(两个对象互相引用,且没有外部引用)。这时需要垃圾回收机制(GC)介入。
循环引用示例:
import gc
import sys
class Node:
def __init__(self, name):
self.name = name
self.next = None # 指向另一个Node
# 创建两个节点,形成循环引用
a = Node("A")
b = Node("B")
a.next = b # A引用B
b.next = a # B引用A
# 此时a和b的引用计数都是2(自身变量 + 对方的引用)
print(sys.getrefcount(a)) # 3(a、b.next、临时引用)
print(sys.getrefcount(b)) # 3(b、a.next、临时引用)
# 删除外部引用(a和b变量),注意哦,只是删除引用,Node("A")本身还存在在内存中。
del a
del b
# 此时两个Node对象互相引用,引用计数为1(无法通过引用计数销毁)
# Python 的垃圾回收(GC)默认是自动触发的,我们这里手动触发垃圾回收
gc.collect()
# GC会检测到循环引用并销毁它们,释放内存
print(gc.garbage) # 默认为空列表(已自动清理)
GC 的核心算法:
- 分代回收:将对象按存活时间分为 3 代(0/1/2),存活越久的对象被扫描的频率越低(假设老对象更稳定)。
- 标记 - 清除:先标记所有可达对象(有外部引用的),再清除未被标记的对象(包括循环引用的孤立对象)。
3. 内存池:小对象的高效管理
Python 对小整数(-5~256)和短字符串采用 “缓存池” 机制,避免频繁创建和销毁。对其他小对象(如小于 256 字节的对象),使用内存池(PyMem_Malloc
)管理,减少系统调用开销。
小整数缓存示例:
a = 100
b = 100
print(a is b) # True(指向同一个对象,来自缓存池)
c = 257
d = 257
print(c is d) # 命令行运行False(超过缓存范围,创建新对象),解释器运行是True,
'''
在pycharm里会进行全局可见的常量复用,整数的内存地址分配受解释器优化策略影响,是不稳定的。
这种结果是解释器实现细节导致的,而非 Python 语言的规定。
所以不要依赖is判断整数身份。
'''
实践任务 1:
写一个脚本,监控一个循环引用对象从创建→形成循环→外部引用删除→GC 回收的完整过程,用gc
模块的set_debug
查看回收细节。
二、字节码:Python 代码的 “中间语言”
Python 是解释型语言,但不会直接执行源码,而是先编译为字节码(一种类似汇编的中间代码),再由 Python 虚拟机(PVM)执行。
1. 查看字节码(用dis
模块)
import dis
def add(a, b):
return a + b
# 查看函数的字节码
dis.dis(add)
输出解析:
3 0 RESUME 0
4 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
'''
1. 第一行:3 0 RESUME 0
3:表示该指令对应源码中的第 3 行(函数定义后的第一行有效代码)。
0:指令在字节码序列中的偏移量(从 0 开始计数,单位是字节)。
RESUME 0:Python 3.11 新增的函数初始化指令,作用是:
设置函数执行的上下文(如异常处理框架、调用栈信息)。
参数0表示 “普通函数调用”(区别于协程、生成器等特殊调用类型)。
替代了旧版本中多个初始化指令(如SETUP_FINALLY),提升函数启动效率。
2. 第二行:4 2 LOAD_FAST 0 (a)
4:对应源码中的第 4 行(return a + b这一行)。
2:当前指令的偏移量(距离上一条指令偏移 2 字节)。
LOAD_FAST 0 (a):将局部变量a加载到 “操作数栈”(Python 虚拟机的临时数据存储区)。
LOAD_FAST:专门用于加载局部变量的指令(比加载全局变量的LOAD_GLOBAL更快)。
0:局部变量的索引(在函数的局部变量表中,a的索引为 0,b为 1)。
(a):注释,显示该索引对应的变量名(方便人阅读)。
3. 第三行:4 4 LOAD_FAST 1 (b)
4:仍对应源码第 4 行(同一行代码可能生成多条字节码指令)。
4:当前指令的偏移量(距离上一条指令偏移 2 字节)。
LOAD_FAST 1 (b):将局部变量b加载到操作数栈。
执行后,操作数栈中有两个元素:[a的值, b的值](栈顶是b,栈底是a)。
4. 第四行:4 6 BINARY_OP 0 (+)
4:仍对应源码第 4 行。
6:当前指令的偏移量(距离上一条指令偏移 2 字节)。
BINARY_OP 0 (+):执行二元运算(加法),作用是:
从操作数栈弹出两个元素(b和a)。
执行a + b运算(注意栈的 “后进先出” 特性,弹出顺序是b先、a后,但加法满足交换律)。
将运算结果压回操作数栈(此时栈中只剩a + b的结果)。
参数0是操作码,对应 “加法”(+),其他操作码对应减法(1)、乘法(2)等。
5. 第五行:4 10 RETURN_VALUE
4:仍对应源码第 4 行。
10:当前指令的偏移量(距离上一条指令偏移 4 字节,因BINARY_OP指令较长)。
RETURN_VALUE:将操作数栈顶的元素(a + b的结果)作为函数返回值,并结束函数执行。
整体执行流程:
函数被调用时,先执行RESUME 0初始化执行环境。
依次将a和b加载到操作数栈。
执行加法运算,得到结果并压回栈。
返回栈顶的结果,函数执行结束。
'''
字节码是一系列操作指令(如LOAD_FAST
、BINARY_ADD
),每个指令对应 PVM 的一个操作。
2. 字节码执行的本质:栈式虚拟机
Python 虚拟机是 “栈式结构”,所有操作都通过操作数栈完成:
LOAD_FAST a
:将变量 a 的值压入栈LOAD_FAST b
:将变量 b 的值压入栈BINARY_ADD
:弹出栈顶两个值(a 和 b),计算 a+b,结果压回栈RETURN_VALUE
:弹出栈顶值作为函数返回值
3. 为什么要了解字节码?
- 定位性能瓶颈:通过字节码指令数量和类型,判断代码效率(如循环中的
LOAD_GLOBAL
比LOAD_FAST
慢)。 - 理解语法糖本质:比如
a += b
和a = a + b
的字节码差异(前者可能在原地修改,后者创建新对象)。
实践任务 2:
对比以下两段代码的字节码,解释为什么第二段更快:
# 代码1
def func1():
total = 0
for i in range(1000):
total = total + i
# 代码2
def func2():
total = 0
for i in range(1000):
total += i
今日学习总结
- 内存管理:引用计数是基础,GC 解决循环引用,内存池优化小对象效率。
- 字节码:源码→字节码→PVM 执行,栈式操作是核心,
dis
模块是分析工具。