Python 到底是值传递还是引用传递?一个赋值实验讲透,别再背结论了

结论先行:Python 既不是值传递,也不是引用传递,而是"传对象引用"(pass by object reference)。函数收到的不是实参的副本,也不是实参本身,而是同一个对象的引用------效果上类似"引用的值传递"。所以可变类型(list、dict)在函数里改内容,外面看得见;不可变类型(int、str、tuple)在函数里"改"变量,外面看不见。把这个结论背下来没用,看完下面这个实验,你一辈子忘不了。


1. 一个实验:先说反直觉的现象

python 复制代码
def add_one(x):
    x = x + 1          # 对数字"加一"

def append_one(lst):
    lst.append(1)      # 往列表里塞一个元素

a = 10
add_one(a)
print(a)               # 10!没变

b = [1, 2]
append_one(b)
print(b)               # [1, 2, 1]!变了

同样是把参数传进函数,a 纹丝不动,b 却被改了。如果 Python 是"值传递",b 不该变;如果是"引用传递",a 也该跟着变。两个都不符合------所以这个问题才吵了这么多年。

别急,先记住这个现象:变的是 list,不变的是 int。规律就藏在类型里。

2. 本质:Python 的变量不是"盒子",是"标签"

理解这一切,只需要接受一个设定:

Python 的变量名不存值,它只是一个贴在对象上的标签(名字绑定)。

python 复制代码
a = 10

这行代码的底层动作是:右边先造出一个整数对象 10,然后让名字 a 指向它。a 本身不占地方存"10",它只是个引用,相当于便利贴。

赋值语句 c = a 更不是"复制了一个盒子"------它只是把新标签 c 也贴到同一个对象上:

python 复制代码
a = [1, 2, 3]
c = a
c.append(4)
print(a)      # [1, 2, 3, 4]------a 和 c 贴的是同一个对象

这就是为什么 is 和 == 必须分开:== 比的是对象里的值,is 比的是"是不是同一个对象"。c is a 在这里是 True。

关键认知 :变量是标签,那"改变量"就有两种完全不同的含义------把标签撕下来贴到别的对象上 (重新绑定),还是钻进对象里面改内容。这两件事,对可变/不可变类型的效果天差地别。

3. 传参:传的是"标签的复印件"

调用函数时发生的事,可以用一句话说清:

函数收到的是实参引用的副本------一个新标签,贴到同一个对象上。

python 复制代码
def append_one(lst):     # lst 是新标签,和 b 贴同一个列表
    lst.append(1)        # 钻进对象改内容 → b 看得见

def add_one(x):          # x 是新标签,和 a 贴同一个 10
    x = x + 1            # 撕下 x,贴到一个新对象 11 上 → a 无感

现在回头看第 1 节的两个现象,全部解释通了:

函数里的动作 实际发生了什么 外部看得见吗
lst.append(1) 通过标签钻进对象,改内容 看得见(对象被改了)
x = x + 1 重新绑定:x 指向新对象 看不见(a 还贴着老对象)

注意一个细节 :x = x + 1 里的 + 1 其实也没"修改"整数对象------整数是不可变的,+ 只会造出一个新对象。所以严格说,不可变类型在函数里连"改内容"这个操作都不存在,你只能换绑。

4. mutable vs immutable:一张表说清

类型 常见成员 能"钻进对象改内容"吗 传参后的表现
不可变 int、float、str、tuple、frozenset 不能 函数内重新赋值,外部无感
可变 list、dict、set、bytearray 能 函数内改内容,外部同步看见

这张表就是全部规则。所谓"Python 是值传递还是引用传递"的争论,八成是把这两种表现混为一谈了。

再加一层保险 :即使传进去的是可变对象,只要你在函数里写 lst = [新列表],那也是重新绑定------外部照样看不见。判定标准永远是那句:这行代码是"改内容"还是"换标签"。

5. 经典陷阱:默认参数为什么"记仇"

python 复制代码
def bug(item, target=[]):
    target.append(item)
    return target

print(bug(1))    # [1]
print(bug(2))    # [1, 2]?!说好的默认空列表呢

陷阱根源 :默认参数只在函数定义时求值一次,那个 [] 从始至终是同一个对象------所有没传 target 的调用,都在往同一个列表里塞东西。

正确写法 :用 None 当哨兵,函数体内再创建:

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

这个坑能坑住人,靠的正是"可变对象 + 引用共享"两个知识点叠加。反过来,不可变类型做默认参数(如 def f(n=0))就安全------反正没人能改它。

6. 同类陷阱:+= 不总是等于 +

+= 对可变类型是"就地修改"(调用 __iadd__),对不可变类型是"重新绑定":

python 复制代码
a = [1, 2]
b = a
a += [3]          # 就地改:a 和 b 还是同一个对象
print(b)          # [1, 2, 3]

x = 1
y = x
x += 1            # 重新绑定:y 无感
print(y)          # 1

同样一个 +=,行为分裂成两半。面试官爱问这个,不是爱抠字眼,是在考察你有没有"可变/不可变"这根弦。

7. 一句话总结

  • 变量是贴在对象上的标签 ,赋值是贴新标签,不是复制内容;
  • 传参传的是引用的副本------新标签贴老对象;
  • 能不能影响外部,只看一件事:改内容(可变)还是换标签(重新绑定);
  • 所以 Python 的准确叫法是 传对象引用(pass by object reference)。

把这四句话想明白,比背"值传递/引用传递"任何一个结论都管用。


下一篇:《Python 的垃圾回收到底回收了谁?引用计数+分代回收一次讲清》------一个 del 之后,对象在内存里经历了什么。关注不迷路。

相关推荐
Maxwin1 小时前
AI 生成的视频怎么局部修改?2026 年分章生成 Manim 代码的最新做法
python·aigc
Java后端的Ai之路1 小时前
07React_Props验证器
人工智能·python·react.js·架构·fastapi
Eider19981 小时前
为Loop Engineering装刹车
python·vibe coding·agent设计模式·loopengineering
kaixin_啊啊2 小时前
【零基础学AI】第 9 章——安装 Python 和开发工具
人工智能·python
雨滴AI自动化测试2 小时前
想让你的自动化测试接入AI,这一步必须要有。
python
用户7624117567052 小时前
第八章 异常处理及程序调试
python
源图客2 小时前
Python环境安装与PyCharm的第一个项目
开发语言·python·pycharm
朝朝辞暮i2 小时前
VLA 系统学习第 20 课——Diffusion Policy 为什么要先给动作加噪,再学习把噪声去掉?
人工智能·python·深度学习·transformer·vla
郝学胜_神的一滴2 小时前
Numpy数据处理详解 02:玩转高性能数组,告别低效循环
人工智能·python