又双叒叕踩坑了!还在用 Python 等号 (=)
面试题:又双叒叕踩坑了!还在用 Python 等号 (=)
rows = [[]] * 3
rows[0].append("A")
print(rows)
输出不是:
[['A'], [], []]
而是:
[['A'], ['A'], ['A']]
这题我第一次看到的时候也愣了一下。不是不会 Python,是这个 = 太容易让人按 Java、C++ 那套脑子去理解。
这地方我一般不先讲“变量”“对象”“引用”这些大词,先看内存味更重一点的东西。
a = []
b = aprint(id(a))
print(id(b))
b.append(100)
print(a)
print(b)
结果大概率长这样:
140475018952768
140475018952768
[100]
[100]
两个 id 一样,说明 a 和 b 根本不是两份列表。
b = a 这行,不是把列表复制了一份给 b,只是又贴了一个名字上去。
这就是 Python 里 = 最容易坑人的地方:它做的是绑定,不是复制。
我见过不少人面试时答得很顺:“Python 一切皆对象。”
但一到代码就写成这样:
base_tags = ["new"]
user_tags = base_tagsuser_tags.append("vip")
然后线上一查,所有默认标签都带了 vip。
这种 bug 最烦的地方是它不报错。日志也干净。你查半天业务逻辑,最后发现是两个变量绑在同一个列表上。
我一般会先加一行日志,不看值,先看地址。
print("base_tags", id(base_tags), base_tags)
print("user_tags", id(user_tags), user_tags)
如果两个 id 一样,后面的业务先别看了,大概率就是别名问题。
正确写法看场景。
如果只是一层列表,直接拷贝一份就行:
base_tags = ["new"]
user_tags = base_tags.copy()user_tags.append("vip")
print(base_tags)
print(user_tags)
输出:
['new']
['new', 'vip']
也可以这样写:
user_tags = list(base_tags)
或者:
user_tags = base_tags[:]
这几个在一层列表里问题不大。
但你别急着高兴,坑还没完。
看这个:
tpl = [["read"], ["write"]]
copied = tpl.copy()copied[0].append("admin")
print(tpl)
print(copied)
输出:
[['read', 'admin'], ['write']]
[['read', 'admin'], ['write']]
这就不是 copy() 没生效,而是它只拷贝了外层。
外层列表确实是新的,但里面那两个小列表,还是同一批对象。
这种我一般叫“外壳换了,里面没换”。
想彻底拆开,用 deepcopy:
from copy import deepcopytpl = [["read"], ["write"]]
copied = deepcopy(tpl)
copied[0].append("admin")
print(tpl)
print(copied)
输出:
[['read'], ['write']]
[['read', 'admin'], ['write']]
不过 deepcopy 也别上来就用。
它不是银弹。对象层级深、数据量大时,性能会疼。尤其是接口里拿到一坨配置、订单明细、权限树,随手 deepcopy 一下,压测时 CPU 就会教你做人。
我的习惯是:能明确只改哪一层,就只复制哪一层。
比如只需要复制用户权限列表,不动角色模板:
role_template = {
"role": "operator",
"permissions": ["read", "export"]
}user_role = {
"role": role_template["role"],
"permissions": role_template["permissions"].copy()
}
user_role["permissions"].append("delete")
print(role_template)
print(user_role)
这样比闭着眼 deepcopy(role_template) 更稳一点,也更能看出你知道自己在改什么。
还有一个面试里特别爱绕的点:不可变对象。
x = 10
y = x
y = y + 1print(x)
print(y)
输出:
10
11
这时候很多人又说:“你看,这不就是复制了吗?”
不是。
x = 10 是让 x 绑定到整数对象 10。
y = x 是让 y 也绑定到 10。
y = y + 1 不是把原来的 10 改成 11,而是算出一个新对象 11,再让 y 绑定过去。
整数、字符串、元组这类不可变对象,表现上很像“复制”,但底层思路还是绑定。
真正危险的是可变对象:list、dict、set,还有你自己写的对象。
再看一个面试官很喜欢塞的小坑:
defadd_item(item, bucket=[]):
bucket.append(item)
return bucketprint(add_item("A"))
print(add_item("B"))
print(add_item("C"))
输出:
['A']
['A', 'B']
['A', 'B', 'C']
这段代码我一看到默认参数是 [],基本就不用往下看了。
默认参数只在函数定义时创建一次。后面每次调用,用的都是同一个列表。
改法也简单:
defadd_item(item, bucket=None):
if bucket isNone:
bucket = [] bucket.append(item)
return bucket
这个写法不华丽,但线上安全。
再补一个更隐蔽的,批量初始化二维数组。
board = [[0] * 3] * 3
board[1][1] = 9print(board)
输出:
[[0, 9, 0], [0, 9, 0], [0, 9, 0]]
原因还是同一个:里面三行其实是同一个列表。
要这么写:
board = [[0for _ in range(3)] for _ in range(3)]
board[1][1] = 9print(board)
输出:
[[0, 0, 0], [0, 9, 0], [0, 0, 0]]
这里别嫌列表推导式麻烦,少写那几个字符,后面排查能把人看麻。
所以这道题真要面试回答,我不会只背一句“Python 的赋值是引用传递”。
这个说法还不够准。
更像这样:
= 只是把名字绑定到对象上。
如果对象是可变的,多个名字绑到同一个对象,任何一个名字改里面的内容,其他名字都能看到。
如果想要新对象,就显式拷贝。浅拷贝还是深拷贝,看你要不要连里面的对象一起拆开。
写 Python 最怕的不是语法不会。
是你以为自己复制了一份,实际上只是多贴了一个标签。这个坑不炸则已,一炸基本都在数据被改脏之后。