Python参数传递:值传递还是引用传递?
时间:2025-09-09 20:13:59 103浏览 收藏
深入理解Python参数传递机制是编写高效、健壮代码的关键。本文旨在剖析Python中“传对象引用”的本质,它既非传统的值传递,也非严格的引用传递,而是一种独特的“传赋值”方式。通过变量与对象的标签关系类比,揭示函数内外参数共享对象的底层原理。文章详细对比了可变对象(如列表、字典)和不可变对象(如字符串、元组)在参数传递中的不同表现,以及函数内部操作对外部变量的影响。同时,提供了实用的解决方案,如返回新值、创建浅拷贝或深拷贝,以及明确函数契约,帮助开发者避免因参数传递引发的意外修改,编写更可预测、易于维护的Python代码。理解Python参数传递的精髓,能有效提升代码质量和开发效率。
Python参数传递的核心机制是“传对象引用”,即传递变量所指向对象的引用。函数内外的参数共享同一对象,若对象可变(如列表),内部修改会影响外部;若不可变(如字符串),则内部重新赋值不会影响外部。

Python的参数传递机制,既不是纯粹的“值传递”(pass by value),也不是严格意义上的“引用传递”(pass by reference)。它更准确地说是“传对象引用”(pass by object reference),或者用更直白的方式讲,就是“传赋值”(pass by assignment)。这意味着当你将一个变量作为参数传递给函数时,实际上是把这个变量所指向的“对象”的引用传递了过去,函数内部的参数名会绑定到同一个对象上。
解决方案
理解Python的参数传递机制,关键在于把握“变量是标签,对象是数据”这个核心理念。当我们将一个变量传递给函数时,实际上是将这个标签指向的内存地址(即对象的引用)传递给了函数的形参。这意味着,函数内部的形参和函数外部的实参,最初都指向内存中的同一个对象。
如果这个对象是可变的(mutable,如列表、字典、集合),那么在函数内部对这个对象进行的修改,会直接影响到函数外部的原始对象。因为它们指向的是同一个实际数据。
而如果这个对象是不可变的(immutable,如整数、浮点数、字符串、元组),那么在函数内部,即使你尝试“修改”它,Python实际上会创建一个新的对象,并将函数内部的形参重新绑定到这个新对象上。这时,函数外部的原始变量仍然指向旧的、未被改变的对象。
这个机制巧妙地结合了两种传递方式的特性,既避免了纯粹值传递带来的数据复制开销(尤其对大对象),又通过不可变对象的特性保护了原始数据不被意外修改,同时又允许通过可变对象实现函数对外部状态的直接影响。理解其精髓,能帮助我们写出更健壮、更可预测的代码。
Python参数传递的核心机制究竟是什么?
要深挖Python参数传递的本质,我们需要跳出传统编程语言中“值传递”和“引用传递”的二元对立思维。Python采取的是一种独特的“传对象引用”模式,这与它“一切皆对象”的设计哲学一脉相承。
想象一下,Python中的变量,它们不是存储数据的盒子,更像是贴在数据盒子上的标签。当你写 a = 10 时,你创建了一个值为10的整数对象,然后把标签 a 贴在了这个对象上。当你再写 b = a 时,你并没有复制10这个值,而是让标签 b 也贴在了同一个值为10的整数对象上。
当我们将 a 传递给一个函数 func(x) 时,发生的情况是:函数内部的形参 x 也被贴到了 a 原本指向的那个对象上。此刻,a 和 x 都是指向同一个对象的标签。
def modify_value(param):
print(f"Inside func, param starts as: {param}, id: {id(param)}")
param = 20 # 这里是重新赋值,不是修改原始对象
print(f"Inside func, param becomes: {param}, id: {id(param)}")
my_int = 10
print(f"Outside func, my_int starts as: {my_int}, id: {id(my_int)}")
modify_value(my_int)
print(f"Outside func, my_int after call: {my_int}, id: {id(my_int)}")输出会清晰地展示:my_int 在函数调用前后 id 保持不变,而 param 在函数内部被重新赋值后,其 id 发生了变化。这说明 param = 20 这行代码,并没有改变 my_int 指向的那个 10 对象,而是让 param 这个标签转而去指向了一个新的 20 对象。my_int 依然坚守着它的 10。
这种机制的好处是显而易见的:避免了不必要的内存复制,尤其是在处理大型数据结构时,效率优势非常明显。同时,它也要求我们对可变对象和不可变对象的行为有清晰的认知,否则很容易踩坑。
可变对象与不可变对象在参数传递中有什么不同表现?
这是Python参数传递中最常引起混淆,也最需要深入理解的地方。对象的“可变性”决定了函数内部操作对其外部影响的程度。
不可变对象(Immutable Objects): 包括整数(int)、浮点数(float)、字符串(str)、元组(tuple)、布尔值(bool)等。这些对象一旦创建,它们的值就不能被改变。如果你尝试“修改”一个不可变对象,Python实际上会创建一个新的对象,并让变量指向这个新对象。
当一个不可变对象作为参数传递时:
def change_string(s):
print(f"Inside func, s before change: '{s}', id: {id(s)}")
s = s + " world" # 创建了一个新字符串对象
print(f"Inside func, s after change: '{s}', id: {id(s)}")
my_str = "hello"
print(f"Outside func, my_str before call: '{my_str}', id: {id(my_str)}")
change_string(my_str)
print(f"Outside func, my_str after call: '{my_str}', id: {id(my_str)}")这里,my_str 始终指向最初的 "hello" 对象。函数内部的 s = s + " world" 只是让 s 这个局部标签重新指向了一个新的字符串对象 "hello world",而 my_str 毫发无损。这是因为字符串是不可变的,任何看似“修改”的操作,本质上都是创建新对象。
可变对象(Mutable Objects): 包括列表(list)、字典(dict)、集合(set)等。这些对象在创建后,它们的内容可以被修改。
当一个可变对象作为参数传递时:
def modify_list(l):
print(f"Inside func, l before modification: {l}, id: {id(l)}")
l.append(4) # 直接修改了列表对象的内容
print(f"Inside func, l after modification: {l}, id: {id(l)}")
l = [5, 6, 7] # 重新赋值,l指向了一个新列表
print(f"Inside func, l after re-assignment: {l}, id: {id(l)}")
my_list = [1, 2, 3]
print(f"Outside func, my_list before call: {my_list}, id: {id(my_list)}")
modify_list(my_list)
print(f"Outside func, my_list after call: {my_list}, id: {id(my_list)}")观察输出,你会发现 l.append(4) 这行代码确实改变了 my_list 的内容,因为 l 和 my_list 在那一刻指向的是同一个列表对象。它们的 id 相同,对其中一个的修改会反映在另一个上。然而,l = [5, 6, 7] 这行代码,就像之前不可变对象的例子一样,只是让 l 这个标签重新指向了一个全新的列表对象,这并没有影响 my_list。
所以,关键点在于:是修改对象本身的内容,还是将变量重新绑定到新的对象。 前者会影响外部的可变对象,后者则不会。
如何避免函数内部修改对外部变量产生意外影响?
在处理可变对象作为参数时,我们常常希望函数能完成它的任务,但又不想它“污染”到函数外部的原始数据。这就像你把一份重要文件借给同事,你希望他阅读、分析,但绝对不希望他直接在原件上涂改。这里有几种策略可以帮助我们管理这种行为:
返回新值,而不是修改原值: 这是最推荐的做法,尤其是在函数设计时。让函数接收参数,处理数据,然后返回一个新的处理结果,而不是直接修改传入的参数。这遵循了“纯函数”的思想,提高了代码的可预测性和可测试性。
def add_item_new_list(original_list, item): new_list = list(original_list) # 创建一个副本 new_list.append(item) return new_list my_data = [1, 2, 3] processed_data = add_item_new_list(my_data, 4) print(f"Original data: {my_data}") # [1, 2, 3] print(f"Processed data: {processed_data}") # [1, 2, 3, 4]这种方式清晰地分离了输入和输出,避免了副作用。
创建参数的副本(Shallow Copy 或 Deep Copy): 如果你确实需要在函数内部修改一个可变对象,但又不希望影响原始对象,那么在函数一开始就创建该对象的一个副本是一个好方法。
浅拷贝(Shallow Copy):对于列表,可以使用
list_param[:]或list_param.copy()。对于字典,可以使用dict_param.copy()。浅拷贝会创建一个新的容器对象,但容器内的元素仍然是原始元素的引用。如果元素本身是可变对象,那么修改这些嵌套的可变元素仍然会影响到原始对象。def modify_list_safely(l): local_list = l[:] # 浅拷贝 local_list.append(4) print(f"Inside func, local_list: {local_list}") my_data = [1, 2, 3] modify_list_safely(my_data) print(f"Outside func, my_data: {my_data}") # [1, 2, 3]深拷贝(Deep Copy):当你的可变对象包含嵌套的可变对象(例如,一个列表里包含另一个列表)时,浅拷贝就不够了。你需要使用
copy模块的deepcopy()函数来创建一个完全独立的副本,包括所有嵌套的子对象。import copy def modify_nested_list_safely(l): local_list = copy.deepcopy(l) # 深拷贝 local_list[0].append('x') print(f"Inside func, local_list: {local_list}") my_nested_data = [[1, 2], [3, 4]] modify_nested_list_safely(my_nested_data) print(f"Outside func, my_nested_data: {my_nested_data}") # [[1, 2], [3, 4]]如果没有
deepcopy,my_nested_data[0]也会被修改。
明确函数契约与文档: 有时候,函数设计就是需要修改传入的可变参数,例如一个排序函数
sort_in_place(a_list)。在这种情况下,关键在于清晰地在函数文档字符串(docstring)中说明这种行为,让调用者知道传入的参数会被修改。def sort_in_place(data_list): """ 对传入的列表进行原地排序。 注意:此函数会直接修改传入的列表对象。 """ data_list.sort() numbers = [3, 1, 4, 1, 5, 9] sort_in_place(numbers) print(f"Sorted numbers: {numbers}") # [1, 1, 3, 4, 5, 9]通过清晰的文档,可以避免用户误解函数的行为,从而减少意外的发生。选择哪种策略取决于具体的业务需求和代码设计哲学,但始终牢记可变对象和不可变对象的区别,是编写高质量Python代码的基础。
本篇关于《Python参数传递:值传递还是引用传递?》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于文章的相关知识,请关注golang学习网公众号!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
205 收藏
-
459 收藏
-
143 收藏
-
395 收藏
-
391 收藏
-
269 收藏
-
263 收藏
-
410 收藏
-
131 收藏
-
382 收藏
-
154 收藏
-
251 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习