《Python 高阶教程》003|变量背后不是盒子:名字、对象与引用的本质

为什么很多 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 里的变量不是装数据的盒子,而是指向对象的名字。赋值通常不是复制,而是建立引用关系。真正需要关注的不是变量名本身,而是它背后指向的对象,以及你做的是修改对象,还是重新绑定名字。

相关推荐
豆瓣鸡13 分钟前
算法日记 - Day3
java·开发语言·算法
浦信仿真大讲堂24 分钟前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
萧瑟余晖29 分钟前
Java深入解析篇九之NIO详解
java·网络·nio
The Chosen One9851 小时前
高进度算法模板速记(待完善)
java·前端·算法
Gu Gu Study1 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境2 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境2 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1232 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy12393102162 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb003 小时前
一个项目带你入门AI应用开发01
python