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

相关推荐
m0_747854521 天前
mysql如何设置数据库连接字符编码_修改default-character
jvm·数据库·python
WiChP1 天前
【V0.1B6】从零开始的2D游戏引擎开发之路
java·log4j·游戏引擎
leaves falling1 天前
C/C++ 的内存管理,函数栈帧详讲
java·c语言·c++
文静小土豆1 天前
Java 应用上 K8s 全指南:从部署到治理的生产级实践
java·开发语言·kubernetes
Wyz201210241 天前
如何在 React 中正确将父组件函数传递给子组件并触发调用
jvm·数据库·python
2401_865439631 天前
Go语言如何用logrus_Go语言logrus日志框架教程【技巧】
jvm·数据库·python
西西弗Sisyphus1 天前
Python 在终端里彩色打印
开发语言·python·print·彩色打印
NotFound4861 天前
CSS如何利用Flex实现悬浮的侧边按钮组_利用fixed定位与flex布局组合
jvm·数据库·python
qq_189807031 天前
Golang怎么实现RBAC权限控制_Golang如何用casbin实现基于角色的访问控制系统【教程】
jvm·数据库·python
vegetablec1 天前
CSS如何处理相对定位留下的原本占位空白_认识到相对定位不会脱离文档流,需借助负margin消除视觉空隙
jvm·数据库·python