人人都会AI编程

4.5 可变对象与不可变对象的内存存储差异

更新时间:2026-07-12

理解了内存池和引用计数之后,自然会引出 Python 中一个影响深远的设计:可变对象和不可变对象。它们的区别不仅是“能不能改”,更关键的是在内存中的存储方式不同,这直接决定了变量赋值、函数传参、拷贝等行为的底层表现。

什么是可变与不可变

  • 不可变对象:创建后,其就不能被改变。Python 中的不可变类型包括:intfloatstrtuplefrozensetbytes
  • 可变对象:创建后,其内部状态(值)可以被修改,而对象的内存地址不变。可变类型包括:listdictsetbytearray 以及绝大多数自定义类的实例。

理解这一点很简单:

  • 你无法让一个整数 5 本身变成 6,只能让变量指向一个值为 6 的新整数对象。
  • 但你可以往一个列表里追加元素,列表对象还是原来那个,只是内容变了。

内存存储的底层差异

这个差异的根源在于 Python 对象在内存中的结构。每个对象都有固定的“头部”信息(引用计数、类型指针等),不同之处在于值数据是如何存放的

  • 不可变对象:值直接“嵌”在对象内部,且对象创建后不允许修改这部分内存。以 int 为例,它有固定的内存大小,存储一个整数值;以 str 为例,字符序列被紧凑存储,Python 底层对其做了严格的不可变保护,任何修改操作实际上都会创建一个新对象
  • 可变对象:对象内部通常有一个指向可变内存区域的指针。例如 list 对象内部存储的是一个指向动态数组的指针,数组中的元素是指向其他对象的引用。当你 append 时,如果动态数组空间足够,就直接修改这块内存区域的内容;对象自身的地址并没有变。

用一个形象的比喻:

  • 不可变对象像一块刻了字的石碑,每个石碑上的字永远不变;想要新内容,就得另立一块碑。
  • 可变对象像一块白板,白板还是那块白板(地址不变),但你可以擦掉内容重写。

对开发者的实际影响

  1. 赋值与共享
  • 对于不可变对象,看起来像“复制”的行为,因为修改时必须产生新对象,不会影响到其他变量。
     a = 5
     b = a   # b 也指向 5
     b = b + 1  # 现在 b 指向新对象 6,a 还是 5
     
  • 对于可变对象,多个变量可能指向同一个对象,修改会互相影响。
     a = [1, 2]
     b = a      # b 和 a 指向同一个列表
     b.append(3)
     print(a)   # [1, 2, 3] —— a 也变了
     
  1. 函数传参的本质

Python 的参数传递是传对象引用(有时叫“传引用的值”或“共享传参”)。无论是可变还是不可变,传到函数内部的都是对象的引用。

  • 传入不可变对象时,在函数里无法改变调用者那里的原始对象,因为任何“修改”都是在创建新对象。
  • 传入可变对象时,函数内部对其的修改(如 list.append)会影响到外部原始对象。这是很多 bug 的来源,也是需要小心的地方。
  1. 拷贝与深拷贝

对列表等可变对象做切片 [:] 或调用 copy.copy(),得到的是浅拷贝——新容器对象产生了,但容器内的元素仍然是共享引用。如果元素也是可变对象,对元素的修改会反映到原对象中。要完全隔离,必须使用 copy.deepcopy

  1. 性能与内存占用
  • 不可变对象允许 Python 做优化:小整数缓存、字符串驻留(intern)可以复用对象,节省内存。同时因为不可变,它是可哈希的,可以作为字典的键或集合的元素。
  • 可变对象因为可以原地修改,在频繁增删的场景效率更高,避免了大量临时对象的创建和销毁。但也因为不可哈希,不能用作字典键。
  1. 在容器中的行为
   t = ([1], [2])  # 元组是不可变的,但元素是可变列表
   t[0].append(10)
   print(t)  # ([1, 10], [2]) —— 元组“变了”?
   

严格来说元组本身没变,改变的是它引用的可变对象。这在逻辑上容易造成混乱,用的时候要明确区分。

理解可变与不可变的内存差异,不是为了背概念,而是为了在编码时预判哪些操作会修改原数据、哪些会产生新数据。这个底层认知,会在面向对象、并发编程、性能优化等各个环节反复发挥作用。