可变实参:函数接收可变实参时的2种情况
问题¶
问题
首先,我们有以下两个函数 f 和 g,以及一个列表 ls:
运行这段代码的输出是:
为什么调用 f(ls) 后 ls 的值没有改变,而调用 g(ls) 后 ls 的值却改变了?
分析问题¶
Python 的参数传递机制¶
要理解这个问题,首先需要了解 Python 中参数是如何传递的。Python 中的参数传递是“按对象引用传递”(pass by object reference)。这意味着:
- 当你调用一个函数并传递一个变量时,实际上传递的是该变量所引用的对象的引用(即内存地址)。
- 函数内部的参数是外部变量的一个别名,它们指向同一个对象。
分析函数 f(l)¶
- 调用
f(ls)时,l被赋值为ls的引用,即l和ls都指向列表[4, 5, 6]。 - 在函数内部,执行
l = [1, 2, 3]。这行代码创建了一个新的列表[1, 2, 3],并将l重新绑定到这个新列表。 • 此时,l不再指向原来的[4, 5, 6],而是指向[1, 2, 3]。 • 但是,ls仍然指向原来的[4, 5, 6]。 - 函数结束后,
l这个局部变量被销毁,[1, 2, 3]如果没有其他引用也会被垃圾回收。 ls的值没有被改变,因为它仍然指向原来的列表。
关键点:l = [1, 2, 3] 是重新绑定 l 到一个新对象,而不是修改原来的对象。因此,外部变量 ls 不受影响。
分析函数 g(l)¶
- 调用
g(ls)时,l被赋值为ls的引用,即l和ls都指向列表[4, 5, 6]。 - 在函数内部,执行
l.append(4)。这是在修改l所指向的列表,即向[4, 5, 6]添加元素4。 • 因为l和ls指向同一个列表,所以通过l修改列表会直接影响ls所指向的列表。 - 函数结束后,
l被销毁,但ls仍然指向已经被修改的列表[4, 5, 6, 4]。
关键点:l.append(4) 是修改 l 所指向的对象的内容,而不是重新绑定 l。因此,外部变量 ls 会看到这个修改。
可变与不可变对象
Python 中的对象分为可变(mutable)和不可变(immutable)两类:
• 可变对象:列表(list)、字典(dict)、集合(set)等。可以在原地修改。
• 不可变对象:整数(int)、浮点数(float)、字符串(str)、元组(tuple)等。不能原地修改。
对于 不可变对象,任何修改操作都会创建一个新对象。例如:
对于 可变对象(如列表),可以原地修改,也可以通过赋值创建新对象。
关键区别¶
• f(l) 中的 l = [1, 2, 3] 是赋值操作,它让 l 指向一个新对象,不影响原来的对象。
• g(l) 中的 l.append(4) 是方法调用,它修改了 l 所指向的对象的内容,因此影响所有引用该对象的变量。
类比¶
想象 ls 是一个盒子,里面装着 [4, 5, 6]。
• f(ls):
• 你告诉 f:“这是盒子 ls,你可以叫它 l”。
• f 把 l 标签从原来的盒子撕下来,贴到一个新盒子 [1, 2, 3] 上。
• 原来的盒子 ls 还是 [4, 5, 6]。
• g(ls):
• 你告诉 g:“这是盒子 ls,你可以叫它 l”。
• g 往 l 盒子里放了一个 4。
• 因为 l 和 ls 是同一个盒子,所以 ls 也到了 4。
验证¶
为了验证这一点,可以在函数内部打印 id(l)(对象的内存地址):
输出可能类似于:
| Output | |
|---|---|
可以看到:
• f 中 l 的 ID 在赋值后改变了(指向了新对象)。
• g 中 l 的 ID 始终不变(修改的是同一个对象)。
总结¶
• f(l) 不改变 ls:因为 f 内部将 l 重新绑定到一个新列表,这是赋值操作,不影响原来的 ls。
• g(l) 改变 ls:因为 g 内部通过 append 修改了 l 所指向的列表的内容,而 ls 也指向同一个列表。
进一步思考¶
如果想要 f 也能改变 ls,可以这样做:
这里 l[:] = [1, 2, 3] 是将 l 的内容替换为新列表的元素,而不是让 l 指向新对象。因此 ls 也会被修改。
关键术语¶
• Rebinding(重新绑定):让变量指向一个新对象(如 l = [1, 2, 3])。
• Mutation(修改):改变对象的内容(如 l.append(4))。
理解这两者的区别是掌握 Python 中变量和函数参数行为的关键。