Python里的“赋值”到底是什么意思? 免费编程软件「pythonpycharm」链接https://pan.quark.cn/s/48a86be2fdc0一个让我困惑了三年的问题刚学Python那年我写了这样一段代码a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4] 什么a也变了 print(b) # [1, 2, 3, 4]我当时的心态是我只改了b为什么a也跟着变了更让我崩溃的是这个x 5 y x y y 1 print(x) # 5 —— 这次x没变 print(y) # 6同样都是赋值为什么一种情况变了另一种没变我当时在网上搜了很久得到的答案都是“可变对象和不可变对象的区别”。但这个解释太抽象了我背下来也理解不了。直到有一天我明白了Python赋值的本质变量不是盒子是标签。这个比喻改变了一切。今天我把这个理解的过程分享给你。重新理解“赋值”不是装东西是贴标签大多数人学编程时脑子里都有一个“变量是盒子”的模型a 5 # 把5放进a这个盒子里 b a # 把a盒子里的5复制一份放进b盒子这个模型在大部分情况下能工作但遇到列表、字典时就翻车了。用“盒子模型”解释不了为什么b a之后改b会影响a。正确的理解方式Python的变量不是盒子而是便利贴标签赋值不是在盒子里装东西而是把标签贴到对象上来看这个a [1, 2, 3]这句话的意思是创建一个列表对象[1, 2, 3]然后把标签a贴在这个对象上。b a这句话的意思是把标签b也贴到a当前贴的那个对象上。现在a和b两个标签贴在同一个对象上。b.append(4)append是找到b标签贴着的那个对象然后往里面加个4。因为a也贴在这个对象上所以用a去看的时候自然也能看到那个4。这就是为什么改b会影响a——不是b和a有什么关系而是它们本来贴的就是同一个东西。验证一下用id()看看对象地址Python里有个内置函数id()可以返回对象的唯一标识可以理解为内存地址。我们来验证一下a [1, 2, 3] print(id(a)) # 比如输出 140234567890 b a print(id(b)) # 输出同样的数字说明指向同一个对象 b.append(4) print(id(a)) # 还是那个数字a还是贴在同一个对象上再看不可变对象x 5 print(id(x)) # 比如 140234567123 y x print(id(y)) # 同样的地址 y y 1 print(id(y)) # 新的地址跟x不一样了 print(id(x)) # 没变还是原来的地址关键区别在于列表是可变的你可以修改对象本身往里面加东西对象还是那个对象地址不变整数是不可变的y y 1不是修改了原来的对象而是创建了一个新对象值为6然后把标签y贴到这个新对象上一句话总结变量是标签不是盒子。赋值就是贴标签。这个理解能解释什么解释1为什么函数参数传递这么“奇怪”很多人觉得Python的函数参数传递很诡异有时候函数内部能修改外部变量有时候不能。用“标签模型”一下就明白了def add_one(n): n n 1 print(id(n)) x 5 print(id(x)) # 地址A add_one(x) # 地址B新对象 print(x) # 还是5流程是这样的调用add_one(x)参数n被贴上x当前贴的对象值为5的那个对象n n 1计算516创建新对象6然后把标签n贴到新对象上函数结束标签n消失标签x从头到尾还是贴在5上没有动过再看列表的情况def add_item(lst): lst.append(4) print(id(lst)) my_list [1, 2, 3] print(id(my_list)) # 地址C add_item(my_list) # 地址C同一个对象 print(my_list) # [1, 2, 3, 4] 变了流程参数lst贴上my_list当前贴的对象列表[1,2,3]lst.append(4)找到这个对象往里面加东西对象还是那个对象地址没变函数结束lst标签消失但my_list还是贴在同一个对象上所以能看到修改核心函数参数传递的是对象的地址标签的复制不是对象的复制。这就是为什么很多人说Python是“传对象引用”。解释2为什么两个列表的修改会互相影响original [1, 2, 3] copy original # 这不是复制是贴了两个标签 copy.append(4) print(original) # [1, 2, 3, 4]如果你想要真正的复制需要创建一个新对象original [1, 2, 3] copy original[:] # 切片创建新列表 # 或者 copy original.copy() # 或者 copy list(original) copy.append(4) print(original) # [1, 2, 3] —— 没变 print(copy) # [1, 2, 3, 4]切片original[:]创建了一个新的列表对象里面的元素是原列表元素的引用对于不可变对象没问题对于嵌套列表要小心——这就是浅拷贝的问题。解释3为什么a b 1能用你可能写过这样的代码a b 0用标签模型很好理解创建对象0然后把标签a和标签b都贴上去。等价于a 0 b a # 把b也贴到a贴的那个对象上解释4为什么a, b b, a能交换值Python的交换写法很优雅a 10 b 20 a, b b, a print(a, b) # 20 10背后发生了什么b, a先创建了一个元组(20, 10)这是个临时对象然后把这个元组里的值依次贴给a和b。用标签模型理解右边的表达式先计算出右边的对象元组然后把左边的标签一个个贴到对应的对象上。所以交换不需要临时变量因为本质是贴标签不是倒腾盒子里的东西。可变 vs 不可变到底谁变了回到开头的困惑为什么改b会影响a关键在于对象的类型类型可变性例子修改对象本身列表可变[1,2,3]append(),extend(),pop(), 索引赋值字典可变{a:1}dict[key]value,update()集合可变{1,2,3}add(),remove()整数不可变5没有修改方法浮点数不可变3.14没有修改方法字符串不可变hello没有修改方法元组不可变(1,2,3)没有修改方法布尔不可变True没有修改方法对于可变对象你可以修改对象本身。贴在这个对象上的所有标签都会“看到”这个变化。对于不可变对象你无法修改对象本身。x x 1会创建新对象然后把标签贴过去。其他标签不受影响。有个办法能立刻判断看操作有没有改变对象的内存地址用id()。地址变了就是创建了新对象地址没变就是修改了原对象。# 可变对象——原地修改 lst [1,2,3] print(id(lst)) lst.append(4) print(id(lst)) # 一样的地址 # 不可变对象——创建新对象 s hello print(id(s)) s s world print(id(s)) # 不一样的地址几个让人防不胜防的坑坑1默认参数的陷阱def add_item(item, my_list[]): my_list.append(item) return my_list print(add_item(1)) # [1] print(add_item(2)) # [1, 2] —— 意外 print(add_item(3)) # [1, 2, 3]你期望每次调用都是一个新的空列表但实际用的是同一个列表对象。原因默认参数的值在函数定义时就被创建了。之后每次调用不传参数时默认参数用的就是那个提前创建好的对象。正确做法def add_item(item, my_listNone): if my_list is None: my_list [] my_list.append(item) return my_list坑2浅拷贝 vs 深拷贝original [[1, 2], [3, 4]] shallow original[:] # 浅拷贝 shallow[0].append(99) print(original[0]) # [1, 2, 99] —— 里面的列表还是同一个浅拷贝只复制了外层容器里面的元素还是原来的标签。如果你有一个嵌套结构列表套列表浅拷贝只解决一层。深拷贝才能完全独立import copy deep copy.deepcopy(original) deep[0].append(88) print(original[0]) # 不受影响经验法则如果你的数据结构里只有不可变对象浅拷贝够用。如果有嵌套的可变对象考虑深拷贝。坑3把可变对象当字典的键d {} lst [1, 2] d[lst] value # TypeError: unhashable type: list字典的键必须是不可变的可哈希的。因为如果键是可变的改了它之后字典就找不到这个键了。所以列表不能当字典的键但元组可以d {(1, 2): value} # 元组不可变可以一个面试题测试你的理解猜猜下面这段代码的输出def test(a, b): a a 1 b.append(4) return x 10 y [1, 2, 3] test(x, y) print(x, y)答案是10 [1, 2, 3, 4]x是整数不可变a a 1创建了新对象不影响外面的xy是列表可变b.append(4)修改了对象本身外面的y能看到变化如果这个答案你想对了恭喜你你已经理解了Python赋值的本质。一张图总结想象你有一个白板上面写着一个数字5和一张清单[1,2,3]。不可变对象整数5你贴标签x指向5你贴标签y也指向5你让y指向6新建的——x还是指向5不受影响可变对象列表[1,2,3]你贴标签a指向这张清单你贴标签b也指向同一张清单你在清单上加了一项4——不管你用a还是b看清单都能看到4最后再说一句我第一次理解“变量是标签”这个概念时有种豁然开朗的感觉。之前觉得Python的赋值行为很“诡异”现在觉得它其实很一致、很简单。所有Python的赋值都是贴标签没有例外。整数、字符串、列表、字典、对象——贴的规则都一样区别在于你贴的对象是否允许被修改可变对象可以原地改不可变对象不能这个理解能帮你少写无数个bug。下次你再写b a的时候心里想的不应该是“把a的值复制给b”而是“把b也贴到a贴的那个东西上”。就这一念之差能救你无数次。