Python里的“赋值”到底是什么意思?
免费编程软件「python+pycharm」
链接: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,计算5+1=6,创建新对象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_list=None): 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贴的那个东西上”。
就这一念之差,能救你无数次。
