← 返回题目列表

Python 函数参数传递是值传递还是引用传递?

高频 中等 第 6 / 21 题 更新于 2026/07/27
函数参数对象引用可变对象不可变对象

简化版

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']

itemsnames 一开始指向同一个列表,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,而是 ab 两个名字都贴在同一个列表对象上。

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)

如果外部也持有这个对象的引用,外部就能看到对象内容变了。

面试时可以这样判断:看到 = 通常是在改绑定;看到 appendextenddict[key] = valueset.add 这类操作,通常是在改对象内容。

三、可变对象和不可变对象的差异

Python 常见不可变对象包括:

  • int
  • float
  • bool
  • str
  • tuple
  • frozenset

常见可变对象包括:

  • list
  • dict
  • set
  • bytearray

不可变对象不能原地改变。比如字符串拼接:

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()

如果涉及外层作用域变量,可以用 globalnonlocal,但要谨慎,因为它会增加函数副作用:

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 是纯值传递,所以函数里什么都改不了外部。 如果传入的是列表、字典、集合等可变对象,函数内部调用 appendupdateadd 修改的是同一个对象,外部引用能观察到变化。
  • 追问:list += [1]tuple += (1,) 为什么表现不同? 列表的 += 通常走原地扩展,外部列表会变;元组不可变,+= 会创建新元组并让局部名字重新绑定。
  • 追问:想让函数改变一个整数变量应该怎么写? 最清楚的方式是返回新值并在外部接收,例如 n = increase(n);用 globalnonlocal 可以改外层绑定,但会增加副作用和测试难度。
  • 误区:不可变对象传参更节省,因为不会复制。 Python 调用函数时不会因为对象不可变就复制整个对象,形参仍然绑定到同一个对象;不可变只表示对象内容不能原地修改。
  • 追问:面试回答该用哪个术语最稳? 可以说“共享传参”或“对象引用传递”,并马上补充“引用本身按值传入,形参是新名字”,这样能避开传统 C++ 意义上值传递/引用传递的歧义。

七、加强记忆

记住“形参是新名字,对象可能是同一个”。重新赋值只是换掉函数内部这个名字的绑定;修改可变对象内容,外部才能看到影响。面试回答时用“对象引用传递 / 共享传参”最稳,再配一个列表 append 和重新赋值的对比例子,基本就能讲清楚。