Python 的传参机制是其内存管理和函数设计的重要组成部分,尤其在处理可变和不可变对象时,可能会导致意想不到的行为。本报告将详细探讨 Python 的传参方式(按值还是按引用),并深入分析如何理解“传对象”,涵盖对象引用的概念、可变性对传参的影响,以及开发者如何在实践中管理这些行为。
背景与问题概述
在编程语言中,传参通常分为按值传递(pass by value)和按引用传递(pass by reference)。按值传递意味着函数接收的是参数的副本,修改不会影响原始变量;按引用传递意味着函数接收的是原始变量的引用,修改会影响原始变量。Python 的传参机制与这些传统概念有所不同,官方文档和社区讨论中常提到“按对象引用传递”(call by object reference)或“按赋值传递”(pass by assignment)。
Python 传参的本质:按对象引用传递
根据官方文档和权威资源,Python 的传参方式是按对象引用传递。这意味着当你将参数传递给函数时,函数接收的是指向同一个对象的引用,而不是对象的副本。具体来说:
- 函数的参数成为函数局部命名空间中的一个新变量,这个变量绑定到与调用者传递的对象相同的对象。
- 这种绑定是通过赋值完成的,因此也被称为按赋值传递。
为了理解这一点,我们需要回顾 Python 的对象模型:
- Python 中一切都是对象,变量只是指向对象的引用(reference)。
- 当你执行
x = 5
,x
是一个名称,绑定到整数对象 5。 - 当你调用
func(x)
,函数func
的参数绑定到同一个对象 5。
可变与不可变对象的影响
Python 对象的可变性(mutability)对传参行为有显著影响:
- 不可变对象(immutable objects):如整数(int)、字符串(str)、元组(tuple)。这些对象一旦创建就不能修改。
- 如果函数尝试修改不可变对象的参数(例如重新赋值),实际上是创建了一个新对象,并绑定到参数名称上,但这不会影响调用者的原始变量。
- 例如:
1
2
3
4
5
6def modify_num(num):
num = 10 # 创建新整数对象 10,绑定到 num
x = 5
modify_num(x)
print(x) # 输出 5,原始变量未变 - 在这种情况下,行为类似于按值传递,因为无法修改原始对象。
- 可变对象(mutable objects):如列表(list)、字典(dict)、集合(set)。这些对象可以被修改。
- 如果函数修改可变对象的状态(例如追加列表元素),这些修改会反映到调用者的原始对象上,因为两者引用的是同一个对象。
- 例如:
1
2
3
4
5
6def modify_list(lst):
lst.append(4) # 修改列表,影响原始对象
y = [1, 2, 3]
modify_list(y)
print(y) # 输出 [1, 2, 3, 4],原始列表已改变 - 在这种情况下,行为类似于按引用传递,因为可以修改原始对象。
然而,需要注意的是,如果函数内重新赋值参数(而不是修改对象内容),这不会影响原始变量:
1 | def reassign_list(lst): |
这表明,参数的重新赋值只影响函数内的局部命名空间,不会改变调用者的绑定。
如何理解“传对象”
“传对象”意味着函数接收的是对象的引用,而不是对象本身的副本。以下是关键点:
- Python 中的变量是对象的引用,传递参数时,函数的参数绑定到与调用者相同的对象。
- 函数可以通过这个引用访问对象的内容,并根据对象的可变性决定是否能修改它。
- 如果对象是可变的,函数可以修改其状态,影响原始对象;如果对象是不可变的,函数只能创建新对象,原始对象不受影响。
为了更直观地理解,可以将变量想象为指向对象的标签(label)。传递参数时,函数得到的是同一个标签的副本,但标签指向的对象是共享的:
- 对于可变对象,修改对象内容相当于在同一个对象上操作,所有标签都会看到变化。
- 对于不可变对象,试图修改会创建新对象,函数内的标签指向新对象,而原始标签仍指向旧对象。
按值、引用、对象传递对比与总结
以下表格对比了按值传递、按引用传递与 Python 传参的差异:
机制 | 描述 | Python 示例 | 影响原始变量 |
---|---|---|---|
按值传递(Pass by Value) | 函数接收参数的副本,修改不影响原变量 | num = 10,函数内赋值新值 | 否 |
按引用传递(Pass by Reference) | 函数接收变量的引用,修改影响原变量 | C++ 中的指针或引用传递 | 是 |
Python 按对象引用传递 | 函数接收对象的引用,可变对象可修改 | 列表追加元素,字典修改键值 | 是(可变对象,修改内容) |
Python 的传参机制结合了按值和按引用的特性,具体行为取决于对象的可变性。这种灵活性适合大多数场景,但需要开发者理解对象模型以避免误用。
思考题
1 | def bad_append(new_item, a_list=[]): |
执行两次 print bad_append('one')
的结果是什么?
1 | def bad_append(new_item, a_list=None): |
执行两次 print bad_append('one')
的结果是什么?
参考
- Python 函数中,参数是传值,还是传引用?
- Pass by Reference in Python: Background and Best Practices
- 8.7. Function definitions
- How do I pass a variable by reference?