Python 函数参数传递是值传递还是引用传递?
简化版
Python 更准确地说是“对象引用传递”或“共享传参”。函数接收的是实参对象的引用副本,所以函数内部能修改可变对象本身,但不能通过重新赋值改变外部变量绑定。
详细版
面试里不要简单说 Python 是值传递或引用传递,因为这两种说法都容易误导。Python 的变量本质上是名字,名字绑定到对象;调用函数时,形参会绑定到实参所指向的同一个对象。
关键结论有三点:
- 如果函数内部只是给形参重新赋值,改变的是局部名字的绑定,不会影响外部变量。
- 如果形参指向的是可变对象,并且函数内部修改对象内容,外部可以看到变化。
- 如果形参指向的是不可变对象,函数内部无法原地修改对象,只能让形参绑定到新对象。
例如:
def change_number(x):
x = x + 1
n = 10
change_number(n)
print(n) # 10
这里 x = x + 1 会创建新整数对象,并让局部变量 x 指向它,外部的 n 不受影响。
再看列表:
def append_item(items):
items.append("Python")
names = ["Java"]
append_item(names)
print(names) # ['Java', 'Python']
items 和 names 一开始指向同一个列表,append 修改的是列表对象本身,所以外部能看到变化。
但如果是重新赋值:
def replace_items(items):
items = ["Go", "Rust"]
names = ["Java"]
replace_items(names)
print(names) # ['Java']
这次只是让局部变量 items 指向新列表,原列表没有被替换。
完整版教学
一、先理解变量不是盒子,而是标签
很多人学编程时会把变量想成一个盒子,盒子里装着值。但在 Python 里,更适合把变量理解成“标签”。
a = [1, 2, 3]
b = a
这里不是把列表复制了一份给 b,而是 a 和 b 两个名字都贴在同一个列表对象上。
b.append(4)
print(a) # [1, 2, 3, 4]
因为被修改的是同一个列表对象,所以通过 a 也能看到变化。
函数调用也是同样的道理:
def f(x):
...
f(a)
调用时,形参 x 会绑定到 a 当前指向的对象。也就是说,x 是函数内部的新名字,但对象仍然是同一个对象。
二、重新赋值和修改对象是两回事
这是这道题最容易混的地方。
重新赋值改变的是“名字指向谁”:
def f(x):
x = [100, 200]
这只是让局部变量 x 指向了一个新列表,不会让外部变量也改绑。
修改对象改变的是“对象里面有什么”:
def f(x):
x.append(100)
如果外部也持有这个对象的引用,外部就能看到对象内容变了。
面试时可以这样判断:看到
=通常是在改绑定;看到append、extend、dict[key] = value、set.add这类操作,通常是在改对象内容。
三、可变对象和不可变对象的差异
Python 常见不可变对象包括:
intfloatboolstrtuplefrozenset
常见可变对象包括:
listdictsetbytearray
不可变对象不能原地改变。比如字符串拼接:
def add_suffix(s):
s += ".txt"
name = "a"
add_suffix(name)
print(name) # a
s += ".txt" 对字符串来说会生成新字符串,并让局部变量 s 指过去。
但列表的 += 可能原地扩展:
def add_more(items):
items += [3, 4]
nums = [1, 2]
add_more(nums)
print(nums) # [1, 2, 3, 4]
列表的 += 通常等价于原地扩展,所以外部列表也变了。这类细节在面试里很容易被追问。
四、函数里想真正改变外部变量怎么办
如果你想让函数返回一个新值,最清晰的方式是返回结果:
def increase(x):
return x + 1
n = 10
n = increase(n)
如果你要修改可变容器,可以直接改容器内容:
def normalize(user):
user["name"] = user["name"].strip()
如果涉及外层作用域变量,可以用 global 或 nonlocal,但要谨慎,因为它会增加函数副作用:
def outer():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
工程代码里,优先返回新值或封装成对象状态,通常比随意修改外层变量更容易维护。
五、用对象关系图判断影响范围
把函数调用画成“名字指向对象”的关系图,很多题就不会混。下面这个过程有 2 个外部名字和 1 个函数内部名字,调用开始时它们指向同一个列表:
调用前:
names ───────┐
▼
["Java"]
调用 append_item(names) 后进入函数:
names ───────┐
▼
items ───> ["Java"]
如果执行 items.append("Python"),箭头没有变,变的是列表对象内容,所以外部 names 看到 ["Java", "Python"]。如果执行 items = ["Go"],只是让局部名字 items 改指向新列表,外部 names 仍指向原列表。这个图也解释了为什么传参本身没有复制列表:复制一份 100 万个元素的列表代价很大,而 Python 只是让形参绑定到同一个对象。
六、常见误区与追问
心法:形参是函数里的新名字,不是外部变量本身;真正共享的是对象。改名字绑定,外面看不到;改同一个可变对象的内容,外面能看到。
- 误区:Python 是纯引用传递,所以函数里重新赋值能改外部变量。 形参拿到的是对象引用的一个新绑定,
x = new_obj只让局部名字x指向新对象,不会让外部名字跟着改绑。 - 误区:Python 是纯值传递,所以函数里什么都改不了外部。 如果传入的是列表、字典、集合等可变对象,函数内部调用
append、update、add修改的是同一个对象,外部引用能观察到变化。 - 追问:
list += [1]和tuple += (1,)为什么表现不同? 列表的+=通常走原地扩展,外部列表会变;元组不可变,+=会创建新元组并让局部名字重新绑定。 - 追问:想让函数改变一个整数变量应该怎么写? 最清楚的方式是返回新值并在外部接收,例如
n = increase(n);用global或nonlocal可以改外层绑定,但会增加副作用和测试难度。 - 误区:不可变对象传参更节省,因为不会复制。 Python 调用函数时不会因为对象不可变就复制整个对象,形参仍然绑定到同一个对象;不可变只表示对象内容不能原地修改。
- 追问:面试回答该用哪个术语最稳? 可以说“共享传参”或“对象引用传递”,并马上补充“引用本身按值传入,形参是新名字”,这样能避开传统 C++ 意义上值传递/引用传递的歧义。
七、加强记忆
记住“形参是新名字,对象可能是同一个”。重新赋值只是换掉函数内部这个名字的绑定;修改可变对象内容,外部才能看到影响。面试回答时用“对象引用传递 / 共享传参”最稳,再配一个列表 append 和重新赋值的对比例子,基本就能讲清楚。