Python里的“赋值”到底是什么意思?

免费编程软件「python+pycharm」 链接:pan.quark.cn/s/48a86be2f...

一个让我困惑了三年的问题

刚学Python那年,我写了这样一段代码:

css 复制代码
a = [1, 2, 3]
b = a
b.append(4)

print(a)  # [1, 2, 3, 4] 什么?a也变了?
print(b)  # [1, 2, 3, 4]

我当时的心态是:我只改了b,为什么a也跟着变了?

更让我崩溃的是这个:

ini 复制代码
x = 5
y = x
y = y + 1

print(x)  # 5 ------ 这次x没变
print(y)  # 6

同样都是赋值,为什么一种情况变了,另一种没变?

我当时在网上搜了很久,得到的答案都是"可变对象和不可变对象的区别"。但这个解释太抽象了,我背下来也理解不了。

直到有一天,我明白了Python赋值的本质:变量不是盒子,是标签

这个比喻改变了一切。今天我把这个理解的过程分享给你。


重新理解"赋值":不是装东西,是贴标签

大多数人学编程时,脑子里都有一个"变量是盒子"的模型:

css 复制代码
a = 5  # 把5放进a这个盒子里
b = a  # 把a盒子里的5复制一份,放进b盒子

这个模型在大部分情况下能工作,但遇到列表、字典时就翻车了。

用"盒子模型"解释不了为什么b = a之后改b会影响a。

正确的理解方式

  • Python的变量不是盒子,而是便利贴(标签)
  • 赋值不是在盒子里装东西,而是把标签贴到对象上

来看这个:

css 复制代码
a = [1, 2, 3]

这句话的意思是:创建一个列表对象[1, 2, 3],然后把标签a贴在这个对象上。

css 复制代码
b = a

这句话的意思是:把标签b也贴到a当前贴的那个对象上。

现在,ab两个标签贴在同一个对象上。

css 复制代码
b.append(4)

append是找到b标签贴着的那个对象,然后往里面加个4。因为a也贴在这个对象上,所以用a去看的时候,自然也能看到那个4。

这就是为什么改b会影响a------不是b和a有什么关系,而是它们本来贴的就是同一个东西。


验证一下:用id()看看对象地址

Python里有个内置函数id(),可以返回对象的唯一标识(可以理解为内存地址)。

我们来验证一下:

bash 复制代码
a = [1, 2, 3]
print(id(a))  # 比如输出 140234567890

b = a
print(id(b))  # 输出同样的数字,说明指向同一个对象

b.append(4)
print(id(a))  # 还是那个数字,a还是贴在同一个对象上

再看不可变对象:

bash 复制代码
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的函数参数传递很诡异,有时候函数内部能修改外部变量,有时候不能。

用"标签模型"一下就明白了:

python 复制代码
def add_one(n):
    n = n + 1
    print(id(n))

x = 5
print(id(x))  # 地址A
add_one(x)    # 地址B(新对象)
print(x)      # 还是5

流程是这样的:

  1. 调用add_one(x),参数n被贴上x当前贴的对象(值为5的那个对象)
  2. n = n + 1,计算5+1=6,创建新对象6,然后把标签n贴到新对象上
  3. 函数结束,标签n消失
  4. 标签x从头到尾还是贴在5上,没有动过

再看列表的情况:

python 复制代码
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] 变了

流程:

  1. 参数lst贴上my_list当前贴的对象(列表[1,2,3]
  2. lst.append(4),找到这个对象,往里面加东西
  3. 对象还是那个对象,地址没变
  4. 函数结束,lst标签消失,但my_list还是贴在同一个对象上,所以能看到修改

核心:函数参数传递的是对象的地址(标签的复制),不是对象的复制。

这就是为什么很多人说Python是"传对象引用"。


解释2:为什么两个列表的修改会互相影响

ini 复制代码
original = [1, 2, 3]
copy = original   # 这不是复制!是贴了两个标签
copy.append(4)

print(original)  # [1, 2, 3, 4]

如果你想要真正的复制,需要创建一个新对象:

scss 复制代码
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能用

你可能写过这样的代码:

css 复制代码
a = b = 0

用标签模型很好理解:创建对象0,然后把标签a和标签b都贴上去。

等价于:

css 复制代码
a = 0
b = a  # 把b也贴到a贴的那个对象上

解释4:为什么a, b = b, a能交换值

Python的交换写法很优雅:

css 复制代码
a = 10
b = 20
a, b = b, a
print(a, b)  # 20 10

背后发生了什么?

b, a先创建了一个元组(20, 10)(这是个临时对象),然后把这个元组里的值依次贴给ab

用标签模型理解:右边的表达式先计算出右边的对象(元组),然后把左边的标签一个个贴到对应的对象上。

所以交换不需要临时变量,因为本质是贴标签,不是倒腾盒子里的东西。


可变 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())。地址变了就是创建了新对象,地址没变就是修改了原对象。

bash 复制代码
# 可变对象------原地修改
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:默认参数的陷阱

python 复制代码
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]

你期望每次调用都是一个新的空列表,但实际用的是同一个列表对象

原因:默认参数的值在函数定义时就被创建了。之后每次调用不传参数时,默认参数用的就是那个提前创建好的对象。

正确做法:

python 复制代码
def add_item(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

坑2:浅拷贝 vs 深拷贝

ini 复制代码
original = [[1, 2], [3, 4]]
shallow = original[:]  # 浅拷贝
shallow[0].append(99)

print(original[0])  # [1, 2, 99] ------ 里面的列表还是同一个!

浅拷贝只复制了外层容器,里面的元素还是原来的标签。如果你有一个嵌套结构(列表套列表),浅拷贝只解决一层。

深拷贝才能完全独立:

go 复制代码
import copy
deep = copy.deepcopy(original)
deep[0].append(88)
print(original[0])  # 不受影响

经验法则:如果你的数据结构里只有不可变对象,浅拷贝够用。如果有嵌套的可变对象,考虑深拷贝。

坑3:把可变对象当字典的键

ini 复制代码
d = {}
lst = [1, 2]
d[lst] = "value"  # TypeError: unhashable type: 'list'

字典的键必须是不可变的(可哈希的)。因为如果键是可变的,改了它之后,字典就找不到这个键了。

所以列表不能当字典的键,但元组可以:

ini 复制代码
d = {(1, 2): "value"}  # 元组不可变,可以

一个面试题测试你的理解

猜猜下面这段代码的输出:

ini 复制代码
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创建了新对象,不影响外面的x
  • y是列表(可变),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贴的那个东西上"

就这一念之差,能救你无数次。

相关推荐
鹅城剑仙2 小时前
Spring Boot 微服务架构设计与最佳实践
spring boot·后端·微服务
Full Stack Developme3 小时前
Spring Integration 教程
java·后端·spring
爱勇宝3 小时前
AI 时代,前端工程师的话语权正在下降?
前端·后端
kymjs张涛3 小时前
一个月,纯VibeCoding,全平台云笔记APP
前端·javascript·后端
星辰_mya3 小时前
autowired和resource区别
java·后端·spring·架构·原理
用户019027581613 小时前
用 Python + backtrader 做专业级策略回测
后端
lazy_ma3 小时前
大模型实操-Spring Boot集成LangChain4j
人工智能·后端
狗头大军之江苏分军3 小时前
前端路由是怎么来的
前端·javascript·后端
云恒要逆袭3 小时前
Java类型转换详解:小数字转大自动跑,大数字转小要小心
java·后端