很多人搜“python变量”,以为这是个入门级话题,翻两页教程就懂了。但说实话,我在带项目、做Code Review的这些年里,见过太多在变量上翻车的案例——不是不会写a = 1,而是搞不清Python的变量到底是怎么工作的。有人在函数里改全局变量改不动,有人把列表传给函数后被悄悄改了,还有人因为默认参数[]排查了一下午。这些坑的根源几乎都指向同一个点:没真正理解Python的变量模型。
这篇文章就把Python变量从底层机制到工程实践完整盘一遍,内容包括“变量是标签还是盒子”这个最核心的认知、可变与不可变对象带来的连锁反应、函数传参、作用域查找、深拷贝浅拷贝,还有跨语言序列化时变量命名引发的哭笑不得的问题。不管你是刚入门想打牢基础,还是已经写了一阵子代码但总在变量相关问题上翻车,这篇都能给你一些收获。
1. 先从最底层的认知说起:变量不是盒子,是标签
1.1 你以前学过的“变量=盒子”模型,在Python里是错的
很多教程会把变量比喻成盒子,赋值就相当于把数据装进盒子里。比如a = [1, 2, 3],脑子里的画面是a这个盒子里装了一个列表。这个比喻在C语言里勉强能用,但在Python里是彻底的误导。
Python的变量,准确的说法是“名字”或“标签”。当你写a = [1, 2, 3]时,实际发生的动作是:先创建一个列表对象[1, 2, 3],然后在命名空间里把名字a关联到这个对象上。也就是说,对象在内存里是独立存在的,变量名只是指向这个对象的一个引用。
用一个最经典的例子就能看出区别:
python复制a = [1, 2, 3]
b = a
b.append(4)
print(a) # 输出 [1, 2, 3, 4]
如果变量是盒子,把a赋值给b相当于把整个列表复制了一份,那改b不应该影响a。但实际结果是a也变了。原因就是b = a只是让b也指向a指向的那个列表对象,两个名字指向同一个内存对象,任何一边通过这个名字修改对象内容,另一边看到的自然就是修改后的结果。
这个认知不纠正过来,后面所有跟变量相关的坑都会反复踩。我建议每个初学者都亲手跑一遍上面的代码,再打印一下id(a)和id(b)看看,两个值是一样的。id()返回的是对象的内存地址,名字不一样但地址一样,说明它们就是同一个对象。
1.2 对象生灭与引用机制:为什么Python不需要声明类型
Python里你从来不用写int a = 1这种声明语句,直接a = 1就行。这背后的原因正是“变量是标签”这个模型:标签本身没有类型,它只是贴到对象上;对象才有类型。a = 1后a指向整数对象,a = "hello"后a改指向字符串对象,整个过程不需要任何类型声明,因为变量本来就是一个通用的名字。
对象的生命周期不归变量管,而由引用计数机制管理。Python的每个对象内部都会记录自己被多少个名字(引用)指着。当a = 1时,整数对象1的引用计数加1;如果a = "hello",原来那个整数对象1的引用计数减1,减到0时这个对象就会被回收释放。
这里有个让很多人意外的现象:Python会对小整数和短字符串做缓存复用。比如你写a = 256和b = 256,打印id(a)和id(b)会发现是相同的,因为256这个对象早就存在内存缓存池里了,a和b不过是贴到了同一个现成对象上。但你写a = 257和b = 257,在某些环境下id就可能不同了。这种细节平时开发不用太在意,但在排查“为什么两个变量明明看着一样,is判断却False”之类问题时,知道有这回事能少走很多弯路。
is比较的是两个名字是否指向同一个对象,==比较的是两个对象的值是否相等,这两者在Python里从来都不是一回事。对小整数和短字符串,因为缓存机制,is经常返回True,很多新手误以为is可以做值比较,直到遇到大整数或长字符串才踩坑。所以经验法则就是:只有当你确实要判断“是不是同一个对象”时用is,比如判断None;判断值是否相等一律用==。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 可变与不可变:Python变量体系里最核心的分水岭
2.1 哪些类型可变,哪些不可变,到底怎么理解“不可变”
Python的内置类型按对象能否被原地修改,分成可变和不可变两大类。
- 不可变:
int、float、str、tuple、bytes、frozenset - 可变:
list、dict、set,还包括大部分自定义类的实例对象
“不可变”的意思不是这个变量不能重新赋值,而是说这个对象本身不能被修改。你不能把"hello"这个字符串对象里的某个字符改成"H",但可以让变量重新指向一个新字符串。
很多人一开始搞混“重新赋值”和“原地修改”,就是因为把变量和对象当成一回事。看一个最典型的例子:
python复制s = "hello"
t = s.replace("h", "H")
print(s) # hello
print(t) # Hello
str.replace()根本没有修改原来的字符串,而是新建了一个字符串对象并返回。如果没把这个返回值接住,原来那个s里的内容永远不变。再看列表:
python复制lst = [1, 2, 3]
lst.append(4)
print(lst) # [1, 2, 3, 4]
list.append()是原地操作,直接修改了lst指向的那个列表对象本身,没有返回新对象。
理解这层之后,很多问题就能解释通了。为什么函数内部对字符串、整数做操作传不出来,但函数内部对列表做append却会影响外部变量?因为前者是创建了新的
