跳转至

可变实参:函数接收可变实参时的2种情况

问题

问题

首先,我们有以下两个函数 fg,以及一个列表 ls

def f(l):
    l = [1, 2, 3]

def g(l):
    l.append(4)

ls = [4, 5, 6]
f(ls)
print(ls)
g(ls)
print(ls)

运行这段代码的输出是:

Output
[4, 5, 6]
[4, 5, 6, 4]

为什么调用 f(ls)ls 的值没有改变,而调用 g(ls)ls 的值却改变了?

分析问题

Python 的参数传递机制

要理解这个问题,首先需要了解 Python 中参数是如何传递的。Python 中的参数传递是“按对象引用传递”(pass by object reference)。这意味着:

  1. 当你调用一个函数并传递一个变量时,实际上传递的是该变量所引用的对象的引用(即内存地址)。
  2. 函数内部的参数是外部变量的一个别名,它们指向同一个对象。

分析函数 f(l)

def f(l):
    l = [1, 2, 3]
  1. 调用 f(ls) 时,l 被赋值为 ls 的引用,即 lls 都指向列表 [4, 5, 6]
  2. 在函数内部,执行 l = [1, 2, 3]。这行代码创建了一个新的列表 [1, 2, 3],并将 l 重新绑定到这个新列表。 • 此时,l 不再指向原来的 [4, 5, 6],而是指向 [1, 2, 3]。 • 但是,ls 仍然指向原来的 [4, 5, 6]
  3. 函数结束后,l 这个局部变量被销毁,[1, 2, 3] 如果没有其他引用也会被垃圾回收。
  4. ls 的值没有被改变,因为它仍然指向原来的列表。

关键点:l = [1, 2, 3] 是重新绑定 l 到一个新对象,而不是修改原来的对象。因此,外部变量 ls 不受影响。

分析函数 g(l)

def g(l):
    l.append(4)
  1. 调用 g(ls) 时,l 被赋值为 ls 的引用,即 lls 都指向列表 [4, 5, 6]
  2. 在函数内部,执行 l.append(4)。这是在修改 l 所指向的列表,即向 [4, 5, 6] 添加元素 4。 • 因为 lls 指向同一个列表,所以通过 l 修改列表会直接影响 ls 所指向的列表。
  3. 函数结束后,l 被销毁,但 ls 仍然指向已经被修改的列表 [4, 5, 6, 4]

关键点:l.append(4) 是修改 l 所指向的对象的内容,而不是重新绑定 l。因此,外部变量 ls 会看到这个修改。

可变与不可变对象

Python 中的对象分为可变(mutable)和不可变(immutable)两类:

• 可变对象:列表(list)、字典(dict)、集合(set)等。可以在原地修改。

• 不可变对象:整数(int)、浮点数(float)、字符串(str)、元组(tuple)等。不能原地修改。

对于 不可变对象,任何修改操作都会创建一个新对象。例如:

1
2
3
4
5
6
def h(x):
    x = x + 1

y = 10
h(y)
print(y)  # 输出 10,因为整数是不可变的,x = x + 1 创建了新对象

对于 可变对象(如列表),可以原地修改,也可以通过赋值创建新对象。

关键区别

f(l) 中的 l = [1, 2, 3] 是赋值操作,它让 l 指向一个新对象,不影响原来的对象。 • g(l) 中的 l.append(4) 是方法调用,它修改了 l 所指向的对象的内容,因此影响所有引用该对象的变量。

类比

想象 ls 是一个盒子,里面装着 [4, 5, 6]

f(ls): • 你告诉 f:“这是盒子 ls,你可以叫它 l”。 • fl 标签从原来的盒子撕下来,贴到一个新盒子 [1, 2, 3] 上。 • 原来的盒子 ls 还是 [4, 5, 6]

g(ls): • 你告诉 g:“这是盒子 ls,你可以叫它 l”。 • gl 盒子里放了一个 4。 • 因为 lls 是同一个盒子,所以 ls 也到了 4

验证

为了验证这一点,可以在函数内部打印 id(l)(对象的内存地址):

def f(l):
    print("Inside f, before assignment, id(l):", id(l))
    l = [1, 2, 3]
    print("Inside f, after assignment, id(l):", id(l))

def g(l):
    print("Inside g, before append, id(l):", id(l))
    l.append(4)
    print("Inside g, after append, id(l):", id(l))

ls = [4, 5, 6]
print("Original ls, id:", id(ls))

f(ls)
g(ls)

输出可能类似于:

Output
1
2
3
4
5
Original ls, id: 2609725351872
Inside f, before assignment, id(l): 2609725351872
Inside f, after assignment, id(l): 2609725202880  # 新对象
Inside g, before append, id(l): 2609725351872
Inside g, after append, id(l): 2609725351872  # 同一个对象

可以看到:

fl 的 ID 在赋值后改变了(指向了新对象)。 • gl 的 ID 始终不变(修改的是同一个对象)。

总结

f(l) 不改变 ls:因为 f 内部将 l 重新绑定到一个新列表,这是赋值操作,不影响原来的 ls

g(l) 改变 ls:因为 g 内部通过 append 修改了 l 所指向的列表的内容,而 ls 也指向同一个列表。

进一步思考

如果想要 f 也能改变 ls,可以这样做:

1
2
3
4
5
6
def f(l):
    l[:] = [1, 2, 3]  # 切片赋值,原地修改列表

ls = [4, 5, 6]
f(ls)
print(ls)  # 输出 [1, 2, 3]

这里 l[:] = [1, 2, 3] 是将 l 的内容替换为新列表的元素,而不是让 l 指向新对象。因此 ls 也会被修改。

关键术语

Rebinding(重新绑定):让变量指向一个新对象(如 l = [1, 2, 3])。

Mutation(修改):改变对象的内容(如 l.append(4))。

理解这两者的区别是掌握 Python 中变量和函数参数行为的关键。