文章目录
- [Python 可变对象与不可变对象(上):赋值陷阱、函数传参与默认参数的暗坑](#Python 可变对象与不可变对象(上):赋值陷阱、函数传参与默认参数的暗坑)
-
- 导入语
- [1 ~> 同一个 `b = a`,结果为什么不一样](#1 ~> 同一个
b = a,结果为什么不一样) -
- [1.1 现象](#1.1 现象)
- [1.2 原因分析](#1.2 原因分析)
- [1.3 判断规则](#1.3 判断规则)
- [1.4 验证](#1.4 验证)
- [2 ~> Python 中各类型的可变性速查](#2 ~> Python 中各类型的可变性速查)
-
- [2.1 完整分类表](#2.1 完整分类表)
- [2.2 一个特别容易错的:tuple 是"真不可变"吗](#2.2 一个特别容易错的:tuple 是"真不可变"吗)
- [3 ~> 函数传参的可变性陷阱](#3 ~> 函数传参的可变性陷阱)
-
- [3.1 现象](#3.1 现象)
- [3.2 原因分析](#3.2 原因分析)
- [3.3 解决方案](#3.3 解决方案)
- [4 ~> 默认参数只初始化一次------我敢说 80% 的 Python 同学都踩过](#4 ~> 默认参数只初始化一次——我敢说 80% 的 Python 同学都踩过)
-
- [4.1 现象](#4.1 现象)
- [4.2 原因分析](#4.2 原因分析)
- [4.3 解决方案](#4.3 解决方案)
- [5 ~> 可变对象做类属性------又一个坑](#5 ~> 可变对象做类属性——又一个坑)
-
- [5.1 现象](#5.1 现象)
- [5.2 原因分析](#5.2 原因分析)
- [5.3 解决方案](#5.3 解决方案)
- [思考 && 总结](#思考 && 总结)
- 结尾
Python 可变对象与不可变对象(上):赋值陷阱、函数传参与默认参数的暗坑
最新推荐文章于 2026-06-15 12:00:00 发布 | 阅读 1.2k 阅读 | 分类:Python基础入门
📖 文章简介: 上篇聚焦可变对象与不可变对象的赋值陷阱和函数传参问题。很多人"知道" int 不可变、list 可变,但到了函数传参、默认参数这种实战场景就踩坑。本文用内存模型图拆解 b=a 后修改 a 为什么有时 b 跟着变有时不变,函数传参到底传的是什么,以及默认参数只初始化一次这个诡异特性。每个结论配 id() 验证代码,文末附真实踩坑案例------一个线上 Bug 因为默认参数是空列表导致的状态污染。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇文章我们讲明白了"变量不是盒子,是标签"。但知道这个之后,第二个问题立刻来了------为什么 b = a 之后,改 a 有时候 b 跟着变(list),有时候 b 不跟着变(int/str)?
这个问题比看起来更深。它不是"Python怪癖",而是 Python 对象模型的核心设计。我见过一个线上订单系统,因为开发同学没搞清这个区别,在函数里传了一个 list 进去,改了里面的元素,结果外面全局变量也跟着被污染了------查了大半天才反应过来。
这篇文章分上下两篇。上篇集中讲赋值陷阱、函数传参和默认参数。下篇讲深浅拷贝和内存图。每一个结论都配了你能自己跑的验证代码。
1 ~> 同一个 b = a,结果为什么不一样
1.1 现象
python
# 情况1:list
a = [1, 2, 3]
b = a
a.append(4)
print(b) # 输出:[1, 2, 3, 4] ← b 变了!
# 情况2:int
a = 100
b = a
a = 200
print(b) # 输出:100 ← b 没变!
同样写 b = a,结果一个变了,一个没变。初学者看到这里基本是崩溃的。
1.2 原因分析
关键不在"赋值"本身,而在于赋值之后你怎么操作 a。
a.append(4)→ 这是"原地修改"------把标签 a 指向的对象本身改了。b 也指向同一个对象,所以 b 能看到变化。a = 200→ 这是"重新赋值"------把标签 a 从旧对象上撕下来,贴到新对象 200 上。b 还贴在旧对象 100 上,所以 b 不变。
bash
"a.append(4)" 改的是对象的内容 ──→ a 和 b 看到的房子被装修过了
"a = 200" 改的是标签的位置 ──→ a 搬家了,b 还住在老房子里
1.3 判断规则
| 操作 | 是原地修改吗 | 对象变了吗 | b 受影响吗 |
|---|---|---|---|
list.append() |
✅ 是 | 原地变 | b 跟着变 |
list.pop() |
✅ 是 | 原地变 | b 跟着变 |
list[0] = x |
✅ 是 | 原地变 | b 跟着变 |
int + 1 |
❌ 不是 | 新建对象 | b 不变 |
str + "x" |
❌ 不是 | 新建对象 | b 不变 |
a = 新值 |
❌ 不是 | 新建对象 | b 不变 |
1.4 验证
python
# 用 id() 看清对象是否变了
a = [1, 2, 3]
b = a
print(f"初始 id(a) = {id(a)}") # 同一个地址
print(f"初始 id(b) = {id(b)}")
a.append(4)
print(f"append后 id(a) = {id(a)}") # 地址没变!说明是原地修改
a = [1, 2, 3]
b = a
print(f"\n初始 id(a) = {id(a)}")
a = [1, 2, 3, 4]
print(f"重新赋值后 id(a) = {id(a)}") # 地址变了!说明是新对象
print(f"重新赋值后 id(b) = {id(b)}") # b 还是旧地址
金句: 可变对象的行为问题,根源在于你没分清"改对象"和"换对象"。改对象所有标签都能看见,换对象只有你自己的标签搬家。
2 ~> Python 中各类型的可变性速查
2.1 完整分类表
| 不可变类型 | 可变类型 |
|---|---|
int、float、bool |
list(列表) |
str(字符串) |
dict(字典) |
tuple(元组) |
set(集合) |
frozenset(冻结集合) |
bytearray(字节数组) |
bytes(字节串) |
自定义类(默认可变) |
2.2 一个特别容易错的:tuple 是"真不可变"吗
python
t = (1, 2, [3, 4]) # 元组装了一个列表
t[2].append(5) # 列表可变,这行能跑
print(t) # 输出:(1, 2, [3, 4, 5])
元组本身的结构确实不可变(你不能 t[0] = 10),但如果元组里装了可变对象,那个可变对象不受 tuple 约束 。你可以改它里面的内容。所以 tuple 的不可变是浅不可变------容器层面不变,内容物层面管不着。
3 ~> 函数传参的可变性陷阱
3.1 现象
这是我 2022 年线上遇到的一个真实 Bug 的精简版:
python
def add_user_to_group(user, group_list):
group_list.append(user)
return group_list
# 默认配置
DEFAULT_GROUP = ["admin"]
# 把张三加进去
current_group = add_user_to_group("张三", DEFAULT_GROUP)
print(current_group) # 输出:['admin', '张三']
print(DEFAULT_GROUP) # 输出:['admin', '张三'] ← ???默认配置被污染了
3.2 原因分析
group_list.append(user) 是原地修改。函数内部改的是传进来的 list 对象本身------而这个对象就是外面的 DEFAULT_GROUP。你以为函数在操作一份拷贝,实际上它直接在原件上盖章。
这在 Java 里是什么行为?Java 里如果传一个 ArrayList,函数里 add 也会影响外部------是一样的道理。但 Java 的 String 是 final class,int 是基本类型直接拷贝值,所以 Java 程序员天然对"传引用"敏感。Python 没有基本类型的设计,一切靠行为判断------这是转过来的同学最容易踩的坑。
3.3 解决方案
python
# 方案一:传入拷贝(安全,推荐)
def add_user_to_group(user, group_list):
group_list = group_list.copy() # 先拷贝再操作
group_list.append(user)
return group_list
# 方案二:函数内部不改动参数(函数式风格)
def add_user_to_group(user, group_list):
return group_list + [user] # + 对列表来说返回新对象
经验: 函数里操作传进来的 list/dict,如果你不期望影响外部,进来第一件事就是
copy()。这个习惯能帮你省掉大把 debug 时间。
4 ~> 默认参数只初始化一次------我敢说 80% 的 Python 同学都踩过
4.1 现象
python
def add_item(item, items=[]): # 默认参数是个空列表
items.append(item)
return items
print(add_item("a")) # 输出:['a']
print(add_item("b")) # 输出:['a', 'b'] ← ???空列表呢?
print(add_item("c")) # 输出:['a', 'b', 'c']
每次调用 add_item 你都期望 items=[] 能给你一个新的空列表,但它却"记住了"上次的结果。啥情况?
4.2 原因分析
Python 的默认参数是在函数定义时计算一次,而不是每次调用时重新计算。 那个 [] 在 def 语句执行时就创建好了,之后每次调用复用同一个列表对象。
bash
def add_item(item, items=[]):
# ↑ 这里的 [] 只执行一次
# 在函数定义时就创建,此后每次调用都用它
第1次调用:items = 同一个 list 对象 → append("a") → 这个 list = ["a"]
第2次调用:items = 同一个 list 对象 → append("b") → 这个 list = ["a", "b"]
第3次调用:items = 同一个 list 对象 → append("c") → 这个 list = ["a", "b", "c"]
这不是 Bug------这是 Python 语言设计者 Guido 有意为之。官方文档明确说这是"feature not a bug"。
4.3 解决方案
python
# ✅ 正确写法:默认值用 None,函数内部判断后创建新对象
def add_item(item, items=None):
if items is None:
items = [] # 每次调用时才创建新列表
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b'] ← 符合预期!
print(add_item("c")) # ['c']
5 ~> 可变对象做类属性------又一个坑
5.1 现象
python
class Classroom:
students = [] # 类属性,所有实例共享
def add_student(self, name):
self.students.append(name)
class1 = Classroom()
class2 = Classroom()
class1.add_student("张三")
print(class2.students) # 输出:['张三'] ← ???class2 怎么也有张三
5.2 原因分析
students = [] 是类属性,在类定义时就创建好了,所有实例共享同一个列表对象 。self.students.append(name) 在 self 上找不到 students 时,会顺着继承链找到类属性------然后原地修改了类属性。所以 class1 加学生,class2 也会看到。
这本质上和默认参数的陷阱逻辑一模一样------可变对象挂在"会被多次用到"的位置上。
5.3 解决方案
python
class Classroom:
def __init__(self):
self.students = [] # 实例属性,每个对象自己的
思考 && 总结
上篇的核心内容------四个关键词:原地修改 / 重新赋值 / 传对象引用 / 默认参数初始化时机。 浓缩成三句话:
- 可变对象支持原地修改,不可变对象不支持。
list.append()改内容不换地址,str + "x"换地址不换内容。分清"改对象"和"换对象",80% 的困惑就没了。 - 函数传参传的是对象引用。 形参和实参指向同一个对象。你在函数里原地修改,外面跟着变。要保护外部数据,进来先
copy()。 - 默认参数在函数定义时只计算一次。 默认值不要用
[]或{},用None替代。
下篇我们深入到深浅拷贝------光知道 copy() 还不够,copy 和 deepcopy 的差异才是我当年 debug 到凌晨三点的元凶。
结尾
各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!
源码骑士 --- Python 全栈 & 系统架构
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!今日源码拆解达成!
🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向
结语:可变与不可变是 Python 对象模型的第一课,也是线上 Bug 的高发区。下篇讲深浅拷贝,敬请期待。不要忘记给博主"一键四连"哦!