Python 中到底有值传递和引用传递吗?
刚接触 Python 的小伙伴,在学习函数参数传递时,很容易被“值传递”和“引用传递”这两个概念绕晕。比如同样是给函数传参,为什么修改列表参数会影响原列表,而修改整数参数却对原变量毫无影响?这是不是意味着 Python 既支持值传递,又支持引用传递?
答案先摆在这里:Python中既没有纯粹的值传递,也没有纯粹的引用传递。它的参数传递机制是一种独特的方式——“传对象引用”(Pass by Object Reference)。想要搞懂这个问题,我们得先跳出“值传递”和“引用传递”的固有思维,从Python的“对象”本质入手。
先复习:值传递 vs 引用传递
在聊Python之前,我们先明确传统编程语言中“值传递”和“引用传递”的定义,这样才能更好地对比理解:
-
值传递(Pass by Value):函数调用时,会创建实参的一个副本,将副本传递给函数。函数内部对参数的修改,只会影响这个副本,不会影响原实参。比如C语言中的普通变量传参。
-
引用传递(Pass by Reference):函数调用时,直接将实参的内存地址传递给函数。函数内部通过这个地址修改的,就是原实参本身。比如C++中的引用传参(&)。
这两个概念的核心区别是:函数是否能直接修改原实参的值。而Python的传参逻辑,用这两个概念都无法完全概括,因为它的关键在于“对象的可变性”。
Python的核心:一切皆对象,对象分可变与不可变
Python中所有的数据类型(int、str、list、dict、tuple等)都是对象。而这些对象又分为两类:
-
不可变对象(Immutable):创建后无法修改其内部值的对象。常见的有:int、str、tuple、float、bool。
-
可变对象(Mutable):创建后可以修改其内部值的对象。常见的有:list、dict、set。
这里的“修改”要注意:不可变对象不是不能重新赋值,而是其本身的内容无法改变。比如a=1,再执行a=2,并不是把原来的1改成了2,而是创建了一个新的int对象2,然后让变量a指向这个新对象。而list的append操作,是直接修改了列表对象本身的内容,没有创建新对象。
Python的传参机制:传对象引用,行为由对象可变性决定
搞懂了可变与不可变对象,再看Python的参数传递就简单了:
当我们调用函数,把变量传递给参数时,传递的是变量所指向的对象的引用(内存地址),而不是变量本身,也不是对象的副本。函数内部拿到这个引用后,具体能做什么,完全取决于这个引用指向的对象是可变还是不可变的。
案例1:传递不可变对象(以int为例)
先看一段代码:
def modify_num(n):
print("函数内修改前,n的地址:", id(n)) # 查看n的内存地址
n = 100 # 重新赋值
print("函数内修改后,n的地址:", id(n)) # 再次查看地址
a = 10
print("调用函数前,a的地址:", id(a))
modify_num(a)
print("调用函数后,a的值:", a) # 输出10,未被修改
运行结果:
调用函数前,a的地址: 140708434756528
函数内修改前,n的地址: 140708434756528
函数内修改后,n的地址: 140708434759248
调用函数后,a的值: 10
分析过程:
-
a=10时,a指向一个int对象10(地址140708434756528)。
-
调用modify_num(a)时,将a指向的对象引用(也就是140708434756528)传递给n,此时n和a指向同一个对象,所以两者地址相同。
-
函数内部执行n=100时,因为int是不可变对象,无法修改原对象10的值,所以会创建一个新的int对象100(地址140708434759248),并让n指向这个新对象。
-
函数结束后,a仍然指向原来的对象10,所以a的值没有变化。
这种情况,从结果上看和“值传递”很像——函数内部修改参数,不影响原实参。但本质不同:值传递是传递副本,而这里传递的是原对象的引用,只是因为对象不可变,无法修改原对象,才产生了类似值传递的效果。
案例2:传递可变对象(以list为例)
再看一段操作列表的代码:
def modify_list(lst):
print("函数内修改前,lst的地址:", id(lst))
lst.append(4) # 修改列表内容
print("函数内修改后,lst的地址:", id(lst))
lst = [10, 20, 30] # 重新赋值
print("函数内重新赋值后,lst的地址:", id(lst))
b = [1, 2, 3]
print("调用函数前,b的地址:", id(b))
modify_list(b)
print("调用函数后,b的值:", b) # 输出[1,2,3,4]
运行结果:
调用函数前,b的地址: 2325885545536
函数内修改前,lst的地址: 2325885545536
函数内修改后,lst的地址: 2325885545536
函数内重新赋值后,lst的地址: 2325885545856
调用函数后,b的值: [1, 2, 3, 4]
分析过程:
-
b=[1,2,3]时,b指向一个list对象(地址2325885545536)。
-
调用modify_list(b)时,将b的对象引用传递给lst,此时lst和b指向同一个列表对象,地址相同。
-
函数内部执行lst.append(4)时,因为list是可变对象,可以直接修改原对象的内容,所以b指向的列表也会同步变化(此时lst仍然指向原地址)。
-
当执行lst=[10,20,30]时,创建了一个新的列表对象,让lst指向新对象(地址2325885545856),但这只是修改了lst的指向,和b没有关系了。
-
函数结束后,b仍然指向原来的列表对象,只是这个对象的内容被append操作修改了,所以b的值变成了[1,2,3,4]。
这种情况,修改对象内容时会影响原实参,看起来像“引用传递”,但重新赋值时却不影响原实参,这又和纯粹的引用传递不同。核心原因还是:传递的是对象引用,修改的是对象本身(可变对象允许修改),而重新赋值只是改变了参数的引用指向,不影响原变量的引用。
总结:跳出“值/引用”,记住这3点
-
Python中没有值传递和引用传递,只有“传对象引用”。
-
函数是否能影响原实参,关键看传递的对象是可变还是不可变:
-
不可变对象(int、str、tuple等):函数内部无法修改原对象,只能创建新对象,效果类似“值传递”。
-
可变对象(list、dict、set等):函数内部可以直接修改原对象的内容,效果类似“引用传递”,但重新赋值参数不会影响原实参。
- 变量的本质是“对象的引用”,赋值操作(=)只是改变变量的引用指向,不会修改原对象。
理解了这一点,以后遇到函数传参的问题就不会困惑了。比如想修改不可变对象的值并影响外部,就只能通过函数返回新对象,再重新赋值给外部变量;而修改可变对象,直接操作即可(但要注意多线程环境下的安全性)。
最后留一个小问题:如果传递的是tuple(不可变对象),但tuple里面包含一个list(可变对象),函数内部修改这个list,会影响原tuple吗?感兴趣的小伙伴可以自己写代码试试~
(注:文档部分内容可能由 AI 生成)
文章版权归作者所有,未经允许请勿转载。

浙公网安备 33010602011771号