(二)从第一阶段的 “内存管理与字节码“ 入手

今天周四不能早下班儿,博主准备摸个鱼~

我们先从 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_FASTBINARY_ADD),每个指令对应 PVM 的一个操作。

2. 字节码执行的本质:栈式虚拟机

Python 虚拟机是 “栈式结构”,所有操作都通过操作数栈完成:

  • LOAD_FAST a:将变量 a 的值压入栈
  • LOAD_FAST b:将变量 b 的值压入栈
  • BINARY_ADD:弹出栈顶两个值(a 和 b),计算 a+b,结果压回栈
  • RETURN_VALUE:弹出栈顶值作为函数返回值
3. 为什么要了解字节码?
  • 定位性能瓶颈:通过字节码指令数量和类型,判断代码效率(如循环中的LOAD_GLOBALLOAD_FAST慢)。
  • 理解语法糖本质:比如a += ba = 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

今日学习总结

  1. 内存管理:引用计数是基础,GC 解决循环引用,内存池优化小对象效率。
  2. 字节码:源码→字节码→PVM 执行,栈式操作是核心,dis模块是分析工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值