Python函数参数传递的真相——为什么你的变量没被修改?
一、一个令人困惑的场景
你刚学 Python 不久,写了这样一段代码:
1 | def try_change(x): |
你期望 num 被改成 100,但它纹丝不动。然而换一种写法:
1 | def try_append(lst): |
列表却被成功修改了。同一个函数、同一种"传参",为什么有时改了外面,有时改不了?Python 到底是"传值"还是"传引用"?
答案是:都不是。Python 使用的是一种更精确的机制——传对象引用(Pass by Object Reference)。
二、核心概念:传对象引用
2.1 变量是"标签",不是"盒子"
在 C++ 中,变量是一个"盒子",里面装着值。赋值就是把新值放进盒子。
在 Python 中,变量是一个"标签"(也叫"名字"),贴在对象上。赋值是把标签撕下来,贴到另一个对象上。
1 | a = 42 # 创建整数对象 42,把标签 a 贴上去 |
2.2 函数调用时发生了什么
当你调用 f(x) 时,Python 做的事情是:
- 在函数的局部框架中创建一个新的标签(形参名)
- 让这个标签指向实参所指向的同一个对象
也就是说,函数内外有两个标签指向同一个对象。这就是"传对象引用"——传的是引用(标签的副本),但引用指向的对象是同一个。
1 | 调用前: num ──→ [42] |
两个标签指向同一个对象,但它们是独立的标签。接下来会发生什么,取决于这个对象是可变的还是不可变的。
三、关键区分:可变对象 vs 不可变对象
3.1 不可变对象:= 断开了联系
不可变对象包括:int、float、str、tuple、frozenset 等。它们的特征是一旦创建,值就无法被原地修改。
1 | def try_change(x): |
图解分析:
1 | 调用前: num ──→ [42] |
= 赋值操作的本质是重新绑定标签,而不是修改对象。对不可变对象来说,你无法修改对象本身,只能让标签指向新对象。而函数内的标签是局部的,它的重新绑定不会影响外面的标签。
3.2 可变对象:原地修改影响外部
可变对象包括:list、dict、set 等。它们的特征是可以在原地修改内容,而不创建新对象。
1 | def try_append(lst): |
图解分析:
1 | 调用前: nums ──→ [1, 2, 3] |
append() 是原地修改操作,它没有创建新对象,而是直接改了共享的那个列表。所以函数外部的 nums 也能看到变化。
3.3 可变对象 + = 赋值:联系断开
如果对可变对象使用 =,结果和不可变对象一样——标签重新绑定,联系断开:
1 | def try_replace(lst): |
图解分析:
1 | 调用前: nums ──→ [1, 2, 3] |
关键结论:决定外部变量是否被修改的,不是对象的类型,而是你做了什么操作——是原地修改,还是 = 重新绑定。
四、深入:= 赋值 vs 原地修改
4.1 = 永远是重新绑定
无论对象是可变还是不可变,= 的语义始终一致:让左边的标签指向右边的新对象。
1 | a = [1, 2] |
4.2 常见的原地修改操作
| 类型 | 原地修改方法 | 说明 |
|---|---|---|
| list | .append(x) |
末尾追加元素 |
| list | .extend(iterable) |
追加多个元素 |
| list | .insert(i, x) |
在位置 i 插入元素 |
| list | .remove(x) |
删除第一个等于 x 的元素 |
| list | .pop([i]) |
弹出并返回元素 |
| list | .sort() |
原地排序 |
| list | .reverse() |
原地反转 |
| list | lst[i] = x |
修改指定位置的元素 |
| dict | .update(other) |
合并字典 |
| dict | .pop(key) |
删除键 |
| dict | d[key] = value |
添加或修改键值对 |
| set | .add(x) |
添加元素 |
| set | .discard(x) |
删除元素 |
这些操作的共同点是:不创建新对象,直接在原对象上改。
4.3 对比:原地排序 vs 返回新排序
1 | # 原地排序 —— 影响原对象 |
五、实践技巧:如何"模拟"引用传递
5.1 方案一:使用 return 返回值(最推荐)
这是最 Pythonic 的方式。函数负责计算,调用者负责更新:
1 | def add_ten(x): |
优点:意图清晰,没有副作用,易于测试和理解。
5.2 方案二:使用可变容器包装
将不可变对象放入可变容器中,通过修改容器来间接修改:
1 | def add_ten(wrapper): |
这种方式在需要返回多个值时也有用:
1 | def swap(wrapper_a, wrapper_b): |
5.3 方案三:利用类的属性
将数据封装在类中,通过方法修改属性:
1 | class Counter: |
这是面向对象编程中管理状态的标准方式,适合需要维护多个相关状态的场景。
六、总结表格
| 操作 | 不可变对象(int/str/tuple) | 可变对象(list/dict/set) |
|---|---|---|
函数内 = 赋值 |
外部不变 ✓ | 外部不变 ✓ |
| 函数内原地修改 | 不可能(不可变) | 外部改变 ✗ |
函数内 lst[i] = x |
不可能(不可变) | 外部改变 ✗ |
函数内 .append() 等 |
不可能(不可变) | 外部改变 ✗ |
核心记忆口诀:
=断联系,改对象传影响。可变能改,不可变只能换。
七、最佳实践
优先使用
return:函数应该通过返回值来传达结果,而不是偷偷修改传入的参数。这使代码的行为可预测、易测试。避免修改可变参数:除非函数的明确目的就是修改传入的可变对象(如
list.sort()),否则不要在函数内部原地修改参数。需要修改时返回副本:如果函数需要基于输入产生新结果,返回一个新对象而非修改原对象:
1 | def add_item(lst, item): |
- 用
id()调试:当你不确定两个变量是否指向同一个对象时,用id()查看:
1 | a = [1, 2] |
Python 的参数传递机制并不复杂,关键在于理解"标签"和"对象"的关系,以及区分"重新绑定"和"原地修改"这两种操作。掌握这两点,所有看似矛盾的行为都能得到清晰的解释。

