Python 中可变对象的“引用赋值”特性——可变对象的“引用传递”

一、踩坑代码

某程序老鸟讲了一个故事:
“2019年夏天,我在做一个推荐系统的用户画像模块。当时写了这样的代码:

# 当时的蠢代码,现在想起来都脸红
default_preferences = []  # 想着所有用户共享一个默认偏好
users = {}
for user_id in user_ids:
    users[user_id] = default_preferences  # 妈的,大坑!

所有用户共享了同一个列表!

用户A喜欢看动作片,结果用户B、C、D全都变成动作片爱好者了。线上故障持续了2个小时,被领导骂得狗血淋头。

那天晚上我查了一夜的资料,才彻底搞懂Python的对象模型。”

解释一下:

# 1. 创建一个列表(内存地址假设为 0x123)
default_preferences = []  
# 2. 循环给 3 个用户赋值
user_ids = [1,2,3]
users = {}
for user_id in user_ids:
    users[user_id] = default_preferences  # 所有用户都指向 0x123 的列表

# 3. 给用户1添加“动作片”偏好
users[1].append("动作片")  

# 4. 查看所有用户的偏好——全变成了动作片!
print(users[1])  # ['动作片'](改的是 0x123 的列表)
print(users[2])  # ['动作片'](指向同一个 0x123)
print(users[3])  # ['动作片'](同样指向 0x123)

本质是:以为给每个用户“分配了一个新列表”,实际是“让所有用户共用同一个列表”——这就是线上故障的根源:用户A修改偏好,等于修改了所有人的偏好。

二、怎么改才对?(避免共享可变对象)

核心思路:给每个用户创建“独立的新列表”,而不是复用同一个列表的引用。有两种常见写法:

1. 循环内每次创建新列表(推荐)

在循环里直接定义空列表,每个用户拿到的都是内存中不同的新列表:

user_ids = [1,2,3]
users = {}
for user_id in user_ids:
    # 每次循环都新建一个空列表(内存地址不同)
    users[user_id] = []  

# 给用户1加偏好,只影响用户1
users[1].append("动作片")
print(users[1])  # ['动作片']
print(users[2])  # [](独立列表,不受影响)

2. 用列表的 copy() 方法(适合有默认值的场景)

如果默认偏好不是空列表(比如有默认值 ["喜剧片"]),可以用 copy() 复制一个新列表给每个用户:

# 有默认值的列表
default_preferences = ["喜剧片"]  
user_ids = [1,2,3]
users = {}
for user_id in user_ids:
    # 复制一个新列表(内存地址不同),避免共享
    users[user_id] = default_preferences.copy()  

# 给用户1加动作片,不影响其他人
users[1].append("动作片")
print(users[1])  # ['喜剧片', '动作片']
print(users[2])  # ['喜剧片'](默认值,未被修改)

三、为什么这个“坑”很容易踩?(新手常见误区)

很多人会误以为“赋值就是复制数据”,但忽略了“可变对象 vs 不可变对象”的区别:

  • 比如给整数赋值 a = 1; b = a; b = 2a 还是 1(不可变对象,赋值是副本);
  • 但给列表赋值 a = []; b = a; b.append(1)a 也会变成 [1](可变对象,赋值是引用)。

你的场景里,“想让所有用户有默认偏好”是合理需求,但错把“共享引用”当成了“共享默认值”——最终导致数据串用,线上故障。

总结

这段代码的“蠢”不是逻辑错,而是对 Python 可变对象的赋值机制理解不到位:

  • 可变对象(列表、字典等)赋值传递“引用”,不是“副本”;
  • 循环中给多个变量赋值同一个可变对象,会导致所有变量共享数据;
  • 解决办法:给每个用户创建独立的可变对象(循环内新建,或用 copy() 复制)。

这种坑很典型,很多新手(甚至工作几年的开发者)都踩过——吃一次线上故障的亏,对“引用传递”的理解就再也忘不掉了~

posted @ 2025-11-03 20:21  wangya216  阅读(17)  评论(0)    收藏  举报