为什么很多 Python 初学者,总会在变量这里栽跟头
学 Python 时,变量是最早接触的概念之一。
很多教程会告诉你,变量就是一个用来存数据的东西。这个说法在入门阶段没问题,但一旦代码稍微复杂一点,它就会开始误导你。
尤其是下面这些现象,很多人第一次看到都会迷糊。
为什么把一个列表赋值给另一个变量,改一个,另一个也变了。
为什么整数看起来不会跟着变,列表却会。
为什么函数里改了参数,外面的值有时会变,有时不会变。
为什么 a = b 之后,看起来像复制,其实很多时候根本不是复制。
这些问题背后,其实都指向同一个核心:Python 里的变量,不是盒子,而是名字。
这个名字指向某个对象。程序运行时,真正被操作的是对象,变量只是贴在对象上的标签。
一旦把这个认识建立起来,后面关于可变对象、不可变对象、参数传递、浅拷贝、深拷贝,都会顺很多。
先把最关键的一句话记住
在 Python 里,变量不是一个装数据的小盒子。
变量更像一个名字,一个标签,一个引用入口。
看下面这行代码:
python
x = 10
很多人脑子里的画面是:创建了一个叫 x 的盒子,里面装了 10。
但 Python 更接近下面这个过程:
先创建对象 10
再让名字 x 指向这个对象
也就是说,核心不是"x 里面装着 10",而是"x 这个名字,绑定到了 10 这个对象上"。
这个差别看起来很小,实际上非常关键。
因为以后你会发现,名字可以换绑,对象也可以被多个名字同时指向。
什么叫名字绑定对象
先看一段最简单的代码。
python
name = "Python"
age = 18
这两行代码做的事情,不是开辟两个盒子。
它做的是:
创建字符串对象 Python
让名字 name 绑定到这个字符串对象
再创建整数对象 18
让名字 age 绑定到这个整数对象
所以,赋值这个动作,本质上不是往盒子里塞东西,而是建立绑定关系。
再看一个更明显的例子:
python
a = 100
b = a
这里发生了什么。
先创建对象 100
名字 a 指向它
然后名字 b 也指向同一个对象
所以,a 和 b 不是两个独立副本,而是两个名字指向同一个对象。
你可以把它想成这样:
对象像一张桌子
a 和 b 像贴在桌子上的两个标签
桌子还是那一张,只是标签有两个。
用 id 看看,变量背后到底是不是同一个对象
Python 提供了一个函数,叫 id()。
它可以帮助我们观察一个名字当前指向的是哪个对象。
看代码:
python
a = 100
b = a
print(id(a))
print(id(b))
如果两次输出一样,说明 a 和 b 指向的是同一个对象。
再看这个例子:
python
a = [1, 2, 3]
b = a
print(id(a))
print(id(b))
你会发现,a 和 b 的 id 也是一样的。
这说明一个事实:
赋值语句 b = a 并没有复制列表。
它只是让 b 也指向了 a 原来指向的那个列表对象。
这就是为什么后面改 a,b 也会变。
为什么列表改一个,另一个也跟着变
来看最经典的一段代码。
python
a = [1, 2, 3]
b = a
a.append(4)
print(a)
print(b)
输出结果是:
python
[1, 2, 3, 4]
[1, 2, 3, 4]
很多人第一反应是,怎么 b 也变了。
其实不是 b 被自动同步了,而是 a 和 b 本来就指向同一个列表对象。
你改的不是变量名,而是变量名背后的那个列表对象。
这里的 append,本质上是在原地修改列表对象。
既然 a 和 b 都指向这个对象,那么无论你通过哪个名字去看,看到的都是修改后的结果。
可以把它理解成:
一块白板上写着 1、2、3
a 和 b 都指向这块白板
a.append(4) 相当于在白板上又写了一个 4
b 再去看这块白板,当然也会看到 4
问题从来不在名字,而在对象是不是同一个。
为什么整数看起来又不会这样
再看一段代码:
python
a = 10
b = a
a = 20
print(a)
print(b)
输出结果是:
python
20
10
很多人会问,刚才列表是一起变,为什么这里 b 没变。
原因不是整数特殊,而是这里发生的动作不一样。
在列表那个例子里:
python
a.append(4)
这是修改对象本身。
而在这个整数例子里:
python
a = 20
这不是修改原来的整数对象,而是让名字 a 重新绑定到了一个新对象 20 上。
原来的对象 10 还在,b 还指着它。
只是 a 已经改去指向别的对象了。
所以结果自然就是:
a 指向 20
b 还指向 10
这个区别特别重要。
修改对象
和
让名字重新绑定到新对象
这是两回事。
可变对象和不可变对象,到底差在哪里
理解上面两个例子后,就可以顺势认识一个非常重要的概念:可变对象和不可变对象。
可变对象,指的是对象创建后,内容可以在原地修改。
常见的有:list、dict、set
不可变对象,指的是对象创建后,内容不能在原地修改。
常见的有:int、float、str、tuple
先看可变对象:
python
data = [1, 2, 3]
data.append(4)
print(data)
这里的列表对象本身被改了。
再看不可变对象:
python
text = "abc"
text = text + "def"
print(text)
表面上看像是在原字符串后面加内容。
但实际上,原来的字符串对象并没有被改。
Python 是创建了一个新的字符串对象 abcdef,再让名字 text 指向它。
也就是说,不可变对象不是"不能变名字",而是"不能改对象本身"。
名字随时可以重新绑定。
不能原地改的是对象本身。
看懂这一点,才能真正理解赋值
很多人把赋值理解成复制。
其实在 Python 里,普通赋值大多数时候都不是复制。
python
a = [1, 2, 3]
b = a
这不是把 a 的内容复制一份给 b。
这只是让 b 和 a 指向同一个对象。
如果真想复制,要显式写出来,比如:
python
a = [1, 2, 3]
b = a.copy()
a.append(4)
print(a)
print(b)
输出结果:
python
[1, 2, 3, 4]
[1, 2, 3]
这才叫创建了一个新的列表对象。
所以你以后只要看到等号,不要立刻脑补成复制。
先问自己一句:
这是在建立引用关系,还是在创建新对象。
这句话能帮你避开很多坑。
名字不是对象本身,名字只是访问对象的入口
再看一个稍微绕一点的例子:
python
x = [1, 2]
y = x
z = y
现在 x、y、z 三个名字都指向同一个列表对象。
如果你执行:
python
z.append(3)
那么 x、y、z 看到的结果都会变成:
python
[1, 2, 3]
因为真正变的是那个列表对象,不是某个名字。
这说明,名字只是入口。
对象才是实体。
多个名字可以指向同一个对象。
一个名字也可以在不同时间指向不同对象。
所以,写 Python 时一定要养成一个习惯:
少盯着变量名,多想它背后指向的是谁。
函数传参时,传的到底是什么
变量问题最容易让人混乱的另一个场景,就是函数调用。
很多人会问,Python 函数参数到底是值传递,还是引用传递。
这个问题如果硬套其他语言的说法,容易越想越乱。
更适合 Python 的理解方式是:
函数调用时,传进去的是对象引用。
函数内部会用新的名字,绑定到传入的对象上。
看代码:
python
def show(data):
print(id(data))
items = [1, 2, 3]
print(id(items))
show(items)
如果两次 id 一样,说明函数参数 data 和外部变量 items,指向的是同一个对象。
这就是为什么函数里改列表,外面也会受影响。
python
def add_item(data):
data.append(4)
items = [1, 2, 3]
add_item(items)
print(items)
输出:
python
[1, 2, 3, 4]
这里不是函数把结果返回给了外面。
而是函数内部和外部,本来操作的就是同一个列表对象。
为什么有时函数里改了参数,外面又不变
再看一个例子:
python
def change_number(n):
n = 100
x = 10
change_number(x)
print(x)
输出还是:
python
10
这又让很多人迷糊了。
原因和前面一样。
函数内部的 n,最开始确实和 x 指向同一个整数对象 10。
但执行 n = 100 时,不是修改对象 10,而是让名字 n 重新绑定到了新对象 100。
外面的 x 仍然指向原来的 10,当然不会变。
所以这里不是"函数对整数无效",而是"重新绑定名字不会影响外部名字"。
再对比一个列表例子:
python
def change_list(data):
data = [100, 200]
nums = [1, 2, 3]
change_list(nums)
print(nums)
输出还是:
python
[1, 2, 3]
为什么这里列表也没变。
因为你不是在改原列表,而是让函数里的名字 data 去指向一个新列表了。
外面的 nums 还是原来的那个对象。
但如果写成这样:
python
def change_list(data):
data.append(4)
nums = [1, 2, 3]
change_list(nums)
print(nums)
输出就会变成:
python
[1, 2, 3, 4]
因为 append 是修改原对象。
你会发现,问题的关键从来不是参数类型本身,而是你到底做了哪种操作:
是修改对象
还是重新绑定名字
一句话讲透函数参数为什么总让人混乱
函数参数看起来复杂,其实可以压缩成一句话:
函数收到的是对象,函数里的参数名只是新的标签。
改对象,外面可能受影响。
改标签,外面通常不受影响。
这个理解比背"值传递""引用传递"更适合 Python。
再看一个字典例子,把感觉彻底建立起来
python
user = {"name": "Tom", "age": 20}
info = user
info["age"] = 21
print(user)
print(info)
输出:
python
{'name': 'Tom', 'age': 21}
{'name': 'Tom', 'age': 21}
原因和列表完全一样。
user 和 info 指向同一个字典对象。
如果改成这样:
python
user = {"name": "Tom", "age": 20}
info = user
info = {"name": "Jack", "age": 30}
print(user)
print(info)
输出:
python
{'name': 'Tom', 'age': 20}
{'name': 'Jack', 'age': 30}
因为这里不是改原字典,而是让 info 重新绑定到新字典。
这个模式你只要彻底看懂,后面很多现象都会自动解释通。
为什么有些 bug 明明没报错,却特别难查
变量、对象、引用没搞清时,最容易出现一种 bug:代码能跑,但结果悄悄错了。
比如:
python
def add_default_tag(tags=[]):
tags.append("python")
return tags
print(add_default_tag())
print(add_default_tag())
print(add_default_tag())
很多人第一次看到输出会吓一跳:
python
['python']
['python', 'python']
['python', 'python', 'python']
为什么每次调用都会累加。
因为默认参数 tags=[] 只在函数定义时创建一次。
后面每次调用,如果没传值,都会继续使用同一个列表对象。
这本质上还是引用问题。
不是每次都新建了一个空列表,而是一直在用同一个对象。
正确写法通常是:
python
def add_default_tag(tags=None):
if tags is None:
tags = []
tags.append("python")
return tags
print(add_default_tag())
print(add_default_tag())
print(add_default_tag())
这样每次才会得到新的列表。
这个坑之所以经典,就是因为很多人一直把变量理解成盒子,意识不到背后其实是对象复用。
is 和 ==,为什么也和对象有关
当你理解变量指向对象后,再看 is 和 == 就不难了。
== 比较的是值是否相等。
is 比较的是是不是同一个对象。
看代码:
python
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)
输出通常是:
python
True
False
为什么。
因为两个列表内容一样,所以 == 为 True。
但它们是两个不同的列表对象,所以 is 为 False。
再看:
python
a = [1, 2, 3]
b = a
print(a == b)
print(a is b)
输出通常是:
python
True
True
因为内容一样,而且本来就是同一个对象。
所以以后判断"值一样没有",用 ==。
判断"是不是同一个对象",用 is。
尤其在判断 None 时,要用 is:
python
if value is None:
print("没有值")
因为这里判断的是对象身份,而不是一般意义上的值比较。
删除变量名,不等于删除对象
再看一个容易忽略的现象:
python
a = [1, 2, 3]
b = a
del a
print(b)
输出仍然是:
python
[1, 2, 3]
为什么。
因为 del a 删除的是名字 a,不是对象本身。
只要还有别的名字指向这个对象,比如 b,这个对象就还活着。
这进一步说明,名字和对象不是一回事。
名字只是引用入口。
对象是否存在,取决于还有没有引用指向它。
这件事后面学垃圾回收时会继续讲得更透。
写代码时,怎么减少引用带来的坑
理解原理之后,更重要的是养成习惯。
下面这些习惯特别实用。
第一,看到可变对象赋值时,多想一步。
b = a 往往不是复制,而是共享对象。
第二,需要独立副本时,主动复制。
列表用 copy,字典也可以用 copy。复杂嵌套结构后面再学深拷贝。
第三,函数里操作参数时,分清楚你是要修改原对象,还是只在函数内部临时使用。
如果不想影响外部,可以先复制一份再改。
第四,默认参数尽量别直接写可变对象。
像列表、字典、集合,通常都用 None 作为默认值更稳。
第五,调试这类问题时,多用 id() 看对象身份。
一旦看清是不是同一个对象,很多问题会立刻明朗。
把今天这一章浓缩成一个最实用的思维模型
以后你看见变量时,脑子里不要再想成盒子。
应该想成下面这个模型:
对象是真正的数据实体
变量名只是标签
赋值是在建立绑定
多个名字可以指向同一个对象
修改对象会影响所有指向它的名字
重新绑定名字,只影响当前这个名字
这个模型一旦建立起来,你会发现 Python 里很多原本模糊的问题,突然都清楚了。
比如:
为什么列表会联动
为什么字符串不会原地变
为什么函数有时会改到外部数据
为什么 copy 很重要
为什么 is 和 == 不一样
这些本来不是零散知识点,它们本质上都在讲同一件事:名字、对象和引用。
本章小结
Python 里的变量不是装数据的盒子,而是指向对象的名字。赋值通常不是复制,而是建立引用关系。真正需要关注的不是变量名本身,而是它背后指向的对象,以及你做的是修改对象,还是重新绑定名字。