Python函数传参是值传递还是引用传递?可变与不可变对象实例解析

周予安
周予安
发布时间:2026-10-30| 5人浏览过
原创

深度解析Python函数传参机制:深入理解对象引用传递(Call by Object Reference)、可变对象与不可变对象的底层内存模型、id()验证以及常见传参陷阱与防御性编程实践。

在 Python 中,函数传参既不是传统 C/C++ 意义上的值传递(深拷贝内存数据),也不是某些语言里的引用传递(为外部变量创建物理别名)。Python 采用的官方机制称为“对象引用传递”(Pass by Object Reference,亦称 Call by Sharing)。函数调用时传递的始终是“对象的内存地址引用”,形参和实参在初始时刻指向同一个对象;后续在函数内部发生的变化,完全由该对象是“可变对象”还是“不可变对象”,以及操作是“原地修改”还是“重新绑定赋值”所决定。

一、机制本质:什么是“对象引用传递”?

在很多初学者的直觉中,变量是一个装放数据的“盒子”。但在 Python 的底层实现中,变量并不是盒子,而是贴在对象上的“标签”(Name Binding)。实际的数据对象独立保存在堆内存中,变量仅仅持有指向该内存块的指针。

使用内置函数 id() 可以获取对象在内存中的唯一地址标识。当两个变量指向同一个对象时,它们的 id() 返回完全一致的数值。

# 验证 Python 的引用绑定机制
a = 1000
b = a
print("a 的内存地址:", id(a))
print("b 的内存地址:", id(b))
print("两者是否为同一对象:", a is b)  # 输出 True

b = b + 1
print("修改后 b 的地址:", id(b))
print("修改后 a 的地址:", id(a))
print("a 与 b 是否依然相同:", a is b)  # 输出 False

当执行 b = b + 1 时,由于整数 1000 是不可变对象,Python 并不会修改原有内存块里的数值,而是在堆中开辟一块新空间存储 1001,并将变量 b 的标签重新贴到了新对象上。此时变量 a 仍然稳固地指向原来的对象。

Python函数传参是值传递还是引用传递?可变与不可变对象实例解析
图 1:Python 变量本质是指针标签,内存对象独立存在并具备不可变性保护

二、执行过程:函数传参的生命周期与推导流程

当调用函数 func(arg) 时,Python 解释器在底层的动作可以明确拆解为以下阶段:

  1. 实参求值:解释器首先计算出实参所指向的对象引用(内存地址)。
  2. 创建形参局部绑定:在函数的局部命名空间(Local Namespace)内创建形参名称,并将该名称直接绑定到实参传入的对象引用上。此时,外部实参和内部形参指向完全相同的内存对象。
  3. 函数体语句执行:
    • 如果对形参执行了原地修改方法(如 append()、extend()、字典键值修改等),原对象在原地发生改变,外部实参自然能观测到变更。
    • 如果对形参执行了赋值语句(如 param = new_value 或 param += ... 处理不可变类型),这仅仅是改变了形参的指向,使其脱离原对象并绑定至新对象。原对象安然无恙,外部实参不受任何影响。
  4. 栈帧退出:函数执行完毕后,函数局部作用域被回收,形参标签随之失效,而外部实参的指向自始至终未曾被强制覆写。
Python函数传参是值传递还是引用传递?可变与不可变对象实例解析
图 2:实参引用传入与函数体内“原地修改” vs “重新绑定”的执行差异全流程

三、实战对比:可变对象与不可变对象传参差异

Python 内置的数据类型明确划分为两大阵营:

  • 不可变对象(Immutable):包括 int、float、bool、str、tuple、frozenset 以及 bytes。这类对象一旦在内存中分配,其内部存储的值即被完全固化,任何看似“修改”的操作都会由解释器在底层创建新对象。
  • 可变对象(Mutable):包括 list、dict、set 以及绝大多数自定义类实例。这类对象支持直接在原有内存地址上增删改内部元素。
分类 代表数据类型 函数内执行 += 或修改行为 外部实参是否受影响 底层原因
不可变对象 int, str, tuple, float 强制生成新对象并重新绑定形参 否(不受影响) 不可变对象无法原地修改,形参指向新内存
可变对象 list, dict, set 调用原地修改方法(如 append) 是(同步改变) 直接在共享内存地址上改写数据
可变对象 list, dict, set 执行全量赋值(如 x = [1, 2]) 否(不受影响) 改变了形参的指针指向,与原对象解绑

1. 不可变对象示例:数值与字符串

def modify_number(x):
    print("函数内刚接收时 id(x):", id(x))
    x += 10
    print("执行 x += 10 后 id(x):", id(x))
    print("函数内部 x 的值:", x)

num = 100
print("调用前外部 num 的 id:", id(num))
modify_number(num)
print("调用后外部 num 的值:", num)
print("调用后外部 num 的 id:", id(num))

# 示例输出:
# 调用前外部 num 的 id: 4321008
# 函数内刚接收时 id(x): 4321008
# 执行 x += 10 后 id(x): 4321328
# 函数内部 x 的值: 110
# 调用后外部 num 的值: 100
# 调用后外部 num 的 id: 4321008

可以看到,在函数刚进入时,id(x) 与外部 id(num) 完全一致;当执行 x += 10 时,由于整数不可变,Python 执行了 x = x + 10 的等价操作,创建了新整数对象并使 x 指向它,原外部变量 num 不受丝毫波及。

2. 可变对象示例:列表的原地修改与重新赋值

def modify_list(target):
    # 原地修改:调用列表底层方法
    target.append("新元素")
    print("原地修改后内部 target id:", id(target))

def rebind_list(target):
    # 重新赋值:开辟全新对象并赋给本地形参
    target = ["全新列表"]
    print("重新赋值后内部 target id:", id(target))

source = ["原始元素"]
print("原始 source id:", id(source))

modify_list(source)
print("调用 modify_list 后的 source:", source)  # 变为: ['原始元素', '新元素']

rebind_list(source)
print("调用 rebind_list 后的 source:", source)  # 依然是: ['原始元素', '新元素']
Python函数传参是值传递还是引用传递?可变与不可变对象实例解析
图 3:不可变与可变对象在参数传递中的根本机制对比矩阵

四、工程避坑与防御性编程实践

理解了 Python 的传参机制后,在日常编写业务代码时,需要重点防范两个高频典型陷阱,并掌握相应的防御手段。

陷阱 1:默认参数使用可变对象(经典坑)

很多开发者容易写出形如 def add_item(item, container=[]) 的代码,以为每次不传 container 时都会新建一个空列表。这是严重的错误认知。

原理说明

Python 中的函数是第一类对象,默认参数表达式只在函数定义阶段(模块加载时)被求值一次,而不是在每次调用时重新计算。因此,定义时创建的默认列表对象会被所有未显式传参的调用永久共享!

# 错误写法:默认参数使用可变列表
def collect_data(val, records=[]):
    records.append(val)
    return records

print(collect_data("A"))  # 输出: ['A']
print(collect_data("B"))  # 输出: ['A', 'B'](残留了前一次调用的数据!)

# 正确标准写法:使用 None 哨兵值与防御性延迟初始化
def collect_data_safe(val, records=None):
    if records is None:
        records = []  # 每次调用动态创建全新独立对象
    records.append(val)
    return records

print(collect_data_safe("A"))  # 输出: ['A']
print(collect_data_safe("B"))  # 输出: ['B'](互不干扰)

陷阱 2:不经意间原地污染调用方数据

当函数职责仅是“分析数据”或“计算汇总”,但其内部为了方便直接调用了类似 param.sort() 或 param.pop() 等原地方法时,外部调用方持有的原列表顺序与元素将被永久破坏。

防御策略:浅拷贝与深拷贝

  • 浅拷贝(Shallow Copy):若数据只有一层结构,可以使用切片 copy_list = data[:] 或 data.copy()。它创建新的外层容器,但内层嵌套对象仍共享引用。
  • 深拷贝(Deep Copy):若数据包含多层嵌套对象(如列表中包含字典或子列表),必须使用 import copy; safe_data = copy.deepcopy(data),递归复制全部层级,完全隔离内外引用。
Python函数传参是值传递还是引用传递?可变与不可变对象实例解析
图 4:Python 函数参数安全处理全景决策树与防御方案

五、全篇认知清单与总结

回顾“Python 函数传参是值传递还是引用传递”这一经典问题,最精准的回答是:Python 是对象引用传递(Call by Object Reference)。在实际工程与理解中,可以归纳为以下四条确定性准则:

  • 传递的永远是引用:实参传入函数时,并未发生完整的数据值复制,形参和实参在调用之初完全共享同一内存对象。
  • 变量只是指针标签:赋值操作符 = 的作用始终是将标签重新绑定到右侧表达式求值后的对象上,绝不会去直接覆写左侧原本绑定的旧内存内容。
  • 不可变对象天然免疫原地修改:包含数字、字符串、元组在内的不可变对象,任何运算修改都会触发新对象的创建,因此外部实参永远不会被函数内部的赋值所篡改。
  • 可变对象的原地方法具有全局副作用:对列表、字典等可变对象调用就地修改方法时,会直接改写共享内存区;若不希望影响调用方,务必在函数入口使用浅拷贝([:])或深拷贝(copy.deepcopy())建立防线,且绝对不要将可变对象作为函数的默认参数。
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。