Python 可变对象与引用穿透:为什么改了"里面的东西"外面也变了?

7 阅读4分钟

一句话结论

Python 变量存的是指针,不是值。容器(list/dict)里装的也是指针。顺着指针改东西,所有持有同一指针的人都看得到。


1. 变量是标签,不是盒子

很多语言(C/Java 基本类型)里,变量像一个盒子,值装在盒子里。Python 不是这样——变量是贴在对象上的标签

a = [1, 2, 3]
b = a
  a ──┐
      ▼
      [1, 2, 3]    ← 内存中只有一个 list 对象
      ▲
  b ──┘

ab 是两张标签,贴在同一个对象上。


2. 赋值 vs 修改:本质区别

赋值(Rebinding)= 撕标签贴到别处

a = [1, 2, 3]
b = a
b = [4, 5, 6]     # b 标签撕下来,贴到新对象上
print(a)          # [1, 2, 3] — a 纹丝不动
  a ───► [1, 2, 3]     ← 原对象没人动
  b ───► [4, 5, 6]     ← 新对象

修改(Mutation)= 顺着标签改对象

a = [1, 2, 3]
b = a
b.append(4)        # 顺着 b 标签找到对象,改了它
print(a)           # [1, 2, 3, 4] — a 也变了!
  a ──┐
      ▼
      [1, 2, 3, 4]    ← 同一个对象被改了
      ▲
  b ──┘

3. 容器里装的也是指针

person = {"name": "张三", "age": 25}
team = [person]

person["age"] = 26
print(team[0]["age"])   # 26 — team 里的也变了!

内存模型:

person ──┐
         ▼
team[0] ─► {"name": "张三", "age": 26}   ← 只有一个 dict

team[0]person 是指向同一个 dict 的两个指针。通过任何一个指针修改 dict,另一个都能看到。


4. 函数传参:同样的规则

def add_tag(user):
    user["vip"] = True    # 修改了传入的 dict

info = {"name": "李四"}
add_tag(info)
print(info)   # {"name": "李四", "vip": True} — 被改了!

函数参数 user 和外部的 info 指向同一个 dict。在函数内修改 dict 属性,外面立刻可见。

但如果函数内部做的是赋值:

def reset_user(user):
    user = {"name": "新人"}   # 只是局部变量换了指向

info = {"name": "李四"}
reset_user(info)
print(info)   # {"name": "李四"} — 没变!

5. 完整分类:哪些操作外部可见?

操作类型示例外部可见?原因
修改 dict 属性d["key"] = val可见顺着指针改对象
修改 list 元素lst[0] = val可见顺着指针改对象
list 追加lst.append(x)可见顺着指针改对象
list 删除lst.pop() / del lst[0]可见顺着指针改对象
dict 删除 keydel d["key"]可见顺着指针改对象
调用修改方法lst.sort() / lst.reverse()可见顺着指针改对象
变量赋值lst = []不可见只是局部标签换了方向

规律:只有 变量 = xxx 这种裸赋值会断开引用。其他一切操作([].、方法调用)都是顺着引用链改对象本身。


6. 嵌套结构:引用链可以很深

company = {
    "departments": [
        {"name": "技术部", "members": ["Alice", "Bob"]},
        {"name": "产品部", "members": ["Charlie"]},
    ]
}

# 取出深层引用
tech = company["departments"][0]
tech["members"].append("Dave")

print(company["departments"][0]["members"])
# ["Alice", "Bob", "Dave"] — 跟着改了

无论嵌套多深,只要顺着引用链走,修改的就是同一个底层对象。


7. 如何"断开"引用?——复制

如果你不想影响原对象,需要显式复制

浅拷贝(一层)

import copy

a = [{"x": 1}, {"x": 2}]
b = a.copy()         # 或 b = list(a) 或 b = a[:]

b.append({"x": 3})   # b 多了一个,a 没有 ✅
b[0]["x"] = 99       # 但 a[0]["x"] 也变了 ❌(里面的 dict 还是共享的)

浅拷贝只复制一层——list 是新的,但里面的 dict 还是共享同一个。

深拷贝(递归所有层)

import copy

a = [{"x": 1}, {"x": 2}]
b = copy.deepcopy(a)

b[0]["x"] = 99
print(a[0]["x"])      # 1 — 完全隔离 ✅

8. 可变 vs 不可变

类型可变?能被"穿透修改"?
list可变
dict可变
set可变
自定义对象可变
int / float不可变不能
str不可变不能
tuple不可变不能(但里面的可变元素能)

tuple 的特殊情况

t = ([1, 2], [3, 4])
t[0].append(5)        # ✅ 合法!tuple 不能换元素,但元素自己可以变
print(t)              # ([1, 2, 5], [3, 4])

9. 判断"是不是同一个对象"

a = [1, 2, 3]
b = a
c = a.copy()

print(a is b)     # True  — 同一个对象
print(a is c)     # False — 不同对象(内容相同但内存地址不同)
print(a == c)     # True  — 值相等
print(id(a) == id(b))  # True
print(id(a) == id(c))  # False
  • is / id() → 判断是不是同一个对象(同一块内存)
  • == → 判断值是否相等(内容一样就行)

10. 一图总结

┌─────────────────────────────────────────────────────────────┐
│                    Python 引用模型                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  变量 ──► 对象                                              │
│                                                             │
│  ┌──────────────────┐     ┌──────────────────┐             │
│  │  赋值 x = y       │     │  修改 x[i] = y   │             │
│  │  • 标签换方向      │     │  • 顺着标签改对象  │             │
│  │  • 原对象不受影响   │     │  • 所有引用者可见  │             │
│  │  • 只影响当前变量   │     │  • 不产生新对象    │             │
│  └──────────────────┘     └──────────────────┘             │
│                                                             │
│  想隔离?→ copy.copy()(浅)或 copy.deepcopy()(深)          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

记忆口诀

赋值换方向,修改穿到底。

= 只是把标签撕下来贴到别处,不碰原对象;
[]. 是顺着标签找到对象动手术,所有人都受影响。