Python可变与不可变数据:C语言视角的深度解析
一、从"盒子"到"标签"
1.1 C语言的"盒子"模型
在 C 语言中,变量是一个有固定大小的"盒子":
1 | int a = 10; // 盒子 a 里装着 10 |
赋值(a = b)就是把 b 盒子里的东西拷贝一份到 a 盒子里。两个盒子互不干扰。
1.2 Python的"标签"模型
Python 的变量更像一张"便利贴"或"标签",而数据本身是堆内存中的一个"对象":
1 | a = 10 # 创建整数对象 10,把标签 a 贴上去 |
赋值(a = b)不是拷贝对象,而是给同一个对象贴上两个标签。这与 C 语言中的指针(int* a)概念非常相似——变量存储的是对象的地址,而非对象本身。Python 隐藏了指针的语法,让一切变得更自动化。
二、核心对比:栈上的值 vs 堆上的对象
2.1 不可变数据:const 对象,修改即重造
不可变对象(如 int, str, tuple)就像 C 语言中用 const 修饰的、分配在堆上的对象。任何"修改"操作,实际上都经历了四步:
malloc一块新的内存- 将旧对象的内容和新值结合,拷贝到新内存中
- 将变量的"标签"(指针)从旧对象指向新对象
- 旧对象的引用计数减一,如果为零则被垃圾回收(相当于
free)
用 id() 函数可以验证——它返回对象的内存地址:
1 | a = 42 |
a += 1 并没有修改原来的整数对象 42,而是创建了一个全新的整数对象 43,然后把标签 a 贴了过去。原来的 42 依然在内存中(直到被回收)。
字符串同理:
1 | s = "hello" |
2.2 可变数据:动态数组结构体,原地修改
可变对象(如 list, dict)就像 C 语言中一个指向动态数组的结构体——包含指针、长度、容量。变量的"标签"指向这个结构体。
当调用 .append() 或修改元素时,相当于通过指针直接修改了堆上那块内存的内容。变量的"标签"没有移动,它依然指向同一个地址,但地址里的数据变了:
1 | lst = [1, 2, 3] |
append 和索引赋值都是原地修改操作,它们直接改了共享的那块内存,标签始终指向同一个对象。
三、经典陷阱:函数参数传递
这是 C 程序员最容易困惑的地方。Python 的参数传递机制叫做传对象引用——形参和实参指向同一个对象,但形参本身是局部的。
3.1 场景A:传入不可变对象
1 | def try_change(num): |
这类似于 C 语言的值传递——函数拿到的是值的拷贝,修改拷贝不影响原件。
3.2 场景B:传入可变对象并原地修改
1 | def try_append(lst): |
这类似于 C 语言的指针传递——函数拿到了地址,通过地址修改了数据,外部自然能看到变化。
3.3 场景C:传入可变对象并重新赋值
1 | def try_replace(lst): |
这类似于在 C 函数内修改一个局部指针变量的指向(int* p = malloc(...);),而不会影响外部的指针。重新赋值 ≠ 原地修改,这是理解 Python 参数传递的关键。
四、元组的"伪"不可变
元组本身不可变,但如果元组中包含可变对象(如列表),列表的内容依然可以被修改:
1 | t = (1, [2, 3], 4) |
元组的不可变性指的是引用地址不变——t[1] 始终指向同一个列表对象。但这个列表对象本身是可变的,它的内容可以随意修改。这就像 C 语言中一个 const 指针——指针本身不能改(不能指向别的地址),但指针指向的数据如果不是 const 的,依然可以修改。
五、哈希性:为什么只有不可变对象能做字典的键
字典的键必须可哈希(hashable),因为字典内部依赖哈希值来快速定位键值对。哈希值的约定是:如果两个对象相等,它们的哈希值必须相同。
如果用可变对象做键,对象被修改后哈希值也会变,字典就再也找不到这个键了——这会破坏字典的内部结构。因此,Python 要求字典的键必须是不可变的,保证哈希值在整个生命周期中不变。
1 | d = {} |
六、总结对比表
| 特性 | C语言类比 | Python行为 | 典型类型 |
|---|---|---|---|
| 变量本质 | 内存盒子 / 指针 | 对象标签 / 引用 | 全部 |
| 不可变数据 | const 对象,修改需 malloc 新内存 |
任何"修改"都创建新对象 | int, str, tuple |
| 可变数据 | 动态数组结构体,可原地修改 | 支持原地修改,内存地址不变 | list, dict, set |
| 函数传参 | 值传递 / 指针传递 | 传对象引用(一种方式) | — |
| 修改方式 | 直接赋值 / 通过指针修改 | = 重新绑定 / 原地方法修改 |
— |
| 内存地址变化 | 不可变:地址变;可变:地址不变 | 同左 | — |
| 哈希性 | — | 不可变可哈希,可变不可哈希 | — |
| 线程安全 | 不可变天然安全 | 不可变天然安全,可变需加锁 | — |
核心记忆:在 Python 中,= 永远是让标签指向新对象,原地修改才是改对象本身。区分这两者,就掌握了可变与不可变的全部秘密。

