Python中列表乘法与引用陷阱:深入理解可变对象行为

Python中列表乘法与引用陷阱:深入理解可变对象行为

本文深入探讨了Python中使用乘法运算符*创建嵌套列表时常见的引用陷阱。通过具体代码示例,揭示了*操作符对可变对象(如列表)执行的是浅层复制,导致所有“副本”实际指向同一内存地址。文章详细解释了元素赋值操作如何进行引用重绑定,而非修改原有对象,最终导致所有共享引用的行显示相同内容。最后,提供了创建独立嵌套列表的正确方法,并强调了理解Python引用机制的重要性。

Python中列表乘法的行为:浅层复制与引用共享

python中,当使用乘法运算符*来“复制”一个包含可变对象的列表时,例如创建嵌套列表,一个常见的误解是它会生成完全独立的副本。然而,*操作符实际上创建的是对原始对象的多个引用,而非独立的深层副本。这意味着所有“复制”出来的元素都指向内存中的同一个可变对象。

让我们通过一个示例来验证这一点。假设我们要创建一个3×2的矩阵,并用None填充。

# 假设 A 是一个用于确定维度的列表,例如 A = [[0,0],[0,0],[0,0]]# 这里的 A 仅用于获取维度,实际内容不影响示例rows = 3cols = 2empty_row = [None] * cols # 创建一个包含两个None的列表empty_matrix = [empty_row] * rows # 将 empty_row 引用三次print("--- 初始状态下的对象ID ---")for i in range(len(empty_matrix)):    print(f"行 {i} 的对象ID: {id(empty_matrix[i])}")    for j in range(len(empty_matrix[0])):        print(f"     元素 ({i},{j}) 的对象ID: {id(empty_matrix[i][j])}", end = ", ")    print()

运行上述代码,你会发现所有行的对象ID都是相同的,这表明empty_matrix中的所有元素都引用了同一个empty_row列表对象。同时,empty_row中的所有None元素也指向同一个None对象(None是不可变单例)。

示例输出可能如下(ID值会因运行环境而异):

--- 初始状态下的对象ID ---行 0 的对象ID: 2856577670848     元素 (0,0) 的对象ID: 140733388238040,      元素 (0,1) 的对象ID: 140733388238040, 行 1 的对象ID: 2856577670848     元素 (1,0) 的对象ID: 140733388238040,      元素 (1,1) 的对象ID: 140733388238040, 行 2 的对象ID: 2856577670848     元素 (2,0) 的对象ID: 140733388238040,      元素 (2,1) 的对象ID: 140733388238040, 

这清晰地表明,empty_matrix[0]、empty_matrix[1]和empty_matrix[2]都指向了同一个列表对象。

立即学习“Python免费学习笔记(深入)”;

可变对象元素的赋值操作:引用重绑定

现在,我们尝试向这个“矩阵”的每个元素赋值。

# 继续上面的 empty_matrix# A 维度不变,假设仍为 3x2rows = 3cols = 2for i in range(rows):    for j in range(cols):        empty_matrix[i][j] = i * 10 + j # 对元素进行赋值print("n--- 赋值后的矩阵内容 ---")for r in empty_matrix:    for c in r:        print(c, end = ", ")    print()print("n--- 赋值后各对象ID ---")for i in range(len(empty_matrix)):    print(f"行 {i} 的对象ID: {id(empty_matrix[i])}")    for j in range(len(empty_matrix[0])):        print(f"     元素 ({i},{j}) 的对象ID: {id(empty_matrix[i][j])}", end = ", ")    print()

你可能会预期输出是:

0, 1,10, 11,20, 21,

然而,实际输出却是:

--- 赋值后的矩阵内容 ---20, 21, 20, 21, 20, 21, 

为什么会这样?这是因为 empty_matrix[i][j] = value 这样的赋值操作,实际上是让 empty_matrix[i] 这个列表中的第 j 个位置的引用指向了一个新的 value 对象,而不是修改了原先被引用的对象。

由于 empty_matrix 中的所有行(empty_matrix[0], empty_matrix[1], empty_matrix[2])都指向了同一个列表对象,当我们在循环中执行 empty_matrix[i][j] = i * 10 + j 时,我们实际上是在反复修改同一个列表对象的元素。每次循环迭代都会更新这个共享列表的元素。因此,当所有赋值操作完成后,这个共享列表的元素将是最后一次迭代(即 i=2)所赋的值。

例如,当 i=0, j=0 时,empty_matrix[0][0] = 0 会将共享列表的第一个元素从 None 变为 0。当 i=1, j=0 时,empty_matrix[1][0] = 10 会将共享列表的第一个元素从 0 变为 10。当 i=2, j=0 时,empty_matrix[2][0] = 20 会将共享列表的第一个元素从 10 变为 20。同理,共享列表的第二个元素最终会变为 21。

所以,最终所有行都显示 [20, 21]。

再观察赋值后的对象ID:

--- 赋值后各对象ID ---行 0 的对象ID: 1782995372160     元素 (0,0) 的对象ID: 1782914902928,      元素 (0,1) 的对象ID: 1782914902960, 行 1 的对象ID: 1782995372160     元素 (1,0) 的对象ID: 1782914902928,      元素 (1,1) 的对象ID: 1782914902960, 行 2 的对象ID: 1782995372160     元素 (2,0) 的对象ID: 1782914902928,      元素 (2,1) 的对象ID: 1782914902960, 

你会发现:

所有行的ID仍然是相同的,这再次证明它们指向同一个列表对象。行内的元素ID已经改变,不再是最初的 None 对象的ID,而是新的整数对象的ID。例如,empty_matrix[0][0]、empty_matrix[1][0]、empty_matrix[2][0] 都指向同一个整数对象 20。

正确创建独立嵌套列表的方法

要创建包含独立列表的嵌套列表(即真正的二维矩阵),每行都应该是一个独立的列表对象。最常见和推荐的方法是使用列表推导式:

rows = 3cols = 2# 方法一:使用列表推导式# 每次循环都会创建一个新的列表对象matrix_correct = [[None for _ in range(cols)] for _ in range(rows)]print("--- 正确创建的矩阵 (列表推导式) ---")for i in range(rows):    print(f"行 {i} 的对象ID: {id(matrix_correct[i])}")    for j in range(cols):        print(f"     元素 ({i},{j}) 的对象ID: {id(matrix_correct[i][j])}", end = ", ")    print()# 进行赋值操作for i in range(rows):    for j in range(cols):        matrix_correct[i][j] = i * 10 + jprint("n--- 赋值后的正确矩阵内容 ---")for r in matrix_correct:    for c in r:        print(c, end = ", ")    print()print("n--- 赋值后正确矩阵的各对象ID ---")for i in range(rows):    print(f"行 {i} 的对象ID: {id(matrix_correct[i])}")    for j in range(cols):        print(f"     元素 ({i},{j}) 的对象ID: {id(matrix_correct[i][j])}", end = ", ")    print()

运行这段代码,你会看到每行的ID都是不同的,证明它们是独立的列表对象。赋值后,输出将符合预期:

--- 赋值后的正确矩阵内容 ---0, 1, 10, 11, 20, 21, 

此时,matrix_correct[0][0]、matrix_correct[1][0]、matrix_correct[2][0] 将分别指向整数对象 0、10、20,它们是不同的对象。

另一种使用循环创建独立嵌套列表的方法:

# 方法二:使用循环matrix_loop = []for _ in range(rows):    matrix_loop.append([None] * cols) # 每次循环都创建一个新的列表对象并添加到 matrix_loop

这种方法与列表推导式达到相同的效果,即每行都是一个独立的列表对象。

总结与注意事项

列表乘法 (*) 的行为*:当对包含可变对象(如列表、字典、自定义类实例)的列表使用 `` 运算符时,它执行的是浅层复制**。这意味着新列表中的所有元素都是对原始可变对象的引用,它们都指向内存中的同一个对象。赋值操作 (=) 的行为:在Python中,list[index] = new_value 这样的赋值操作会重绑定引用。它使 list[index] 指向 new_value 对象,而不是修改 list[index] 原来指向的对象的内容。可变与不可变对象:理解可变对象(列表、字典、集合)和不可变对象(数字、字符串、元组)之间的区别至关重要。对不可变对象的“修改”实际上是创建了一个新对象并重绑定引用。而对可变对象的某些操作(如 list.append(), list.sort(), dict.update())是原地修改对象内容,这些修改会通过所有引用可见。但 list[index] = new_value 仍是重绑定。创建独立副本:对于嵌套列表,创建独立副本的最佳实践是使用列表推导式,如 [[item for item in row] for row in original_matrix] 或 [[initial_value for _ in range(cols)] for _ in range(rows)]。对于更复杂的嵌套结构,可能需要使用 copy 模块中的 copy.deepcopy() 函数来确保所有层级的对象都是独立的副本。

通过深入理解Python的引用机制和赋值操作的本质,可以有效避免在处理复杂数据结构时遇到的常见陷阱,编写出更健壮、可预测的代码。

以上就是Python中列表乘法与引用陷阱:深入理解可变对象行为的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1375373.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月14日 14:56:48
下一篇 2025年12月14日 14:57:13

相关推荐

  • Matplotlib运行时动态切换主题样式:直接操作Figure和Axes对象

    在matplotlib应用中,若尝试使用`plt.style.use()`在图表创建后动态切换主题,会发现其无法生效。本文将深入探讨`plt.style.use()`的适用场景,并提供一种针对已存在图表进行运行时主题切换的有效方法:通过直接修改`figure`和`axes`对象的背景色、边框色等属性…

    好文分享 2025年12月14日
    000
  • 如何实现python中的continue语句?

    continue语句用于跳过当前循环的剩余部分,直接进入下一次循环判断。例如在for i in range(10): if i % 2 == 0: continue; print(i),输出1,3,5,7,9;在while循环中同样适用,如n=0; while n 在Python中,continue…

    2025年12月14日
    000
  • 解决GitHub Actions中N8n容器连接问题的教程

    在github actions中运行docker compose时,n8n容器可能因`localhost`解析问题导致连接失败。本教程将深入探讨在ci/cd环境中,docker容器间通信应使用服务名称而非`localhost`,并指导如何正确配置n8n的环境变量及docker compose卷挂载,…

    2025年12月14日
    000
  • Python Pandas:精确控制浮点数到百分比的转换与格式化

    本教程详细阐述了在python pandas中如何将浮点数转换为具有特定小数位精度的百分比字符串。文章深入解析了python字符串格式化中{:.n%}语法的工作原理,并通过实际代码示例展示了使用.map()方法对pandas series进行高效且准确的格式化操作,确保输出符合预期的舍入规则,从而避…

    2025年12月14日
    000
  • Flask-SQLAlchemy 多对多关系:用户与角色权限管理的正确实践

    本文详细介绍了如何在 Flask 应用中利用 SQLAlchemy 实现用户与角色之间的多对多关系。我们将通过一个博客应用的示例,展示如何正确定义关联表、用户模型和角色模型,并纠正常见的 `InvalidRequestError`,特别是由于模型类命名不规范或关系属性配置错误导致的问题,确保关系配置…

    2025年12月14日
    000
  • Python教程:在多行文本文件中根据关键词查找并打印指定行

    本教程详细介绍了如何使用python在多行文本文件中查找包含特定关键词的行。通过文件逐行读取和字符串包含性检查的组合,我们将展示一种简洁高效的方法来定位并输出所需内容。文章涵盖了文件路径处理、核心代码实现,并提供了进一步优化和扩展的建议,帮助开发者灵活处理文本数据。 在日常编程任务中,我们经常需要处…

    2025年12月14日
    000
  • 大规模数据下Scipy信号相关性直接法:高效计算局部滞后范围

    当处理超大规模数据集时,`scipy.signal.correlate` 的直接法(`method=”direct”`)默认会计算所有可能的滞后,这在仅需局部滞后范围结果时效率低下。对于因数据规模或稀疏性导致 fft 方法不适用的场景,本文提供一种自定义的循环实现方案。该方案…

    2025年12月14日
    000
  • Python Tkinter iconphoto() 方法详解:设置应用程序图标

    本文旨在详细解释 Python Tkinter 中 `iconphoto()` 方法的使用,重点区分 `default` 参数为 `True` 和 `False` 时的行为差异,帮助开发者正确设置应用程序图标。 在 Tkinter 中,iconphoto() 方法用于设置应用程序的窗口图标。该方法接…

    2025年12月14日
    000
  • Python多目标优化:解决复杂座位分配问题的策略与实践

    本文探讨如何利用Python解决复杂的活动座位分配问题,特别是涉及多方偏好和动态变化的场景。我们将深入了解优化、多目标优化及启发式算法的核心概念,并讨论如何构建一个能够平衡宾客偏好与场地优先级,并有效应对突发情况的自动化解决方案。 在活动组织和资源分配场景中,如何高效地为参与者分配座位,同时满足多方…

    2025年12月14日
    000
  • Python FileNotFoundError:文件路径疑难解析与解决方案

    本教程旨在解决python中常见的`filenotfounderror`,特别是当文件路径看似正确却仍然报错时。文章将深入探讨文件相对路径与绝对路径的原理,指导用户如何准确识别当前工作目录和目标文件路径,并提供可靠的代码示例与最佳实践,确保文件能够被成功访问。 理解 FileNotFoundErro…

    2025年12月14日
    000
  • Python浮点数精度与表示:深入理解截断与科学计数法

    本文深入探讨Python浮点数在处理大数字和特定小数位时出现的精度问题及表示行为。我们将解析IEEE 754浮点标准、Python `float.__repr__`的优化机制,以及为何看似“截断”或转换为科学计数法的现象实则是底层浮点表示的固有特性。文章将提供示例并介绍如何使用`decimal`模块…

    2025年12月14日
    000
  • Hatch 虚拟环境存储位置管理与自定义

    hatch 默认将虚拟环境统一存储在其管理的特定数据目录中,而非当前项目根目录,旨在提供更集中的环境管理。本文将深入探讨 hatch 这一设计理念,并详细指导用户如何利用 `–data-dir` 选项自定义虚拟环境的存储路径,实现将虚拟环境创建在项目目录内部,并提供清晰的操作示例。 Ha…

    2025年12月14日
    000
  • 本地加载TensorFlow MNIST .npz数据集教程

    本教程旨在解决tensorflow中因网络连接问题导致mnist数据集无法通过`tf.keras.datasets.mnist.load_data()`在线加载的困境。我们将详细指导用户如何手动下载`mnist.npz`文件,并利用numpy库将其高效、准确地加载到本地环境中,从而确保机器学习项目的…

    2025年12月14日
    000
  • 高效处理超大规模数据集的局部滞后相关性计算

    本文旨在解决在处理亿级规模大型数据集时,使用`scipy.signal.correlate`的`method=”direct”`方法计算全量滞后相关性效率低下,而`method=”fft”`因数据特性不适用,但又仅需计算特定小范围滞后值的问题。我们将提…

    2025年12月14日
    000
  • Python nonlocal关键字使用指南:何时以及为何需要它

    nonlocal关键字在python中用于指示一个变量赋值操作应作用于最近的非全局(enclosing)作用域中的变量,而非在当前函数内创建新的局部变量。理解其核心在于区分对变量的“重新赋值”与对可变对象“内容修改”:只有当你想在内层函数中改变外层函数变量所引用的对象时,才需要使用nonlocal。…

    2025年12月14日
    000
  • C++ OpenSSL AES CBC解密乱码问题解析与EVP API最佳实践

    本文深入探讨了使用C++ OpenSSL低级API(如`AES_cbc_encrypt`)进行AES CBC模式加密时,解密数据开头出现乱码的问题。核心原因在于低级API会原地修改初始化向量(IV),导致解密时无法获取正确的IV。文章强调应避免使用这些低级函数,并详细介绍了OpenSSL推荐的高级E…

    2025年12月14日
    000
  • PyCharm项目文件夹在macOS中消失的解决方案:文件权限配置指南

    本文旨在解决macos用户在使用pycharm时遇到的项目文件夹从项目面板消失的问题。核心原因通常是macos的文件权限限制,而非pycharm本身的bug。教程将详细指导如何在系统设置中为pycharm配置正确的访问权限,确保项目文件正常显示和操作,从而解决此困扰。 问题描述 许多PyCharm用…

    2025年12月14日
    000
  • Python 3.12 type 关键字定义类型别名的优势与应用

    Python 3.12 引入了 `type` 关键字用于定义类型别名,旨在提供更简洁的泛型类型参数语法、支持类型别名的惰性求值,并使其与普通变量区分更明确。尽管它带来了诸多优势,尤其是在静态类型检查方面,但与传统的简单赋值方式或 `typing.TypeAlias` 相比,新语法并非完全的替代品,例…

    2025年12月14日
    000
  • 解决C++ OpenSSL低级AES解密乱码:推荐使用EVP API

    本文旨在解决C++ OpenSSL低级AES函数(如`AES_cbc_encrypt`)在与其他语言(如Python)进行数据加解密时出现的乱码问题。文章将深入分析低级API的潜在陷阱,并强烈推荐使用OpenSSL的高级EVP API,提供详细的C++ EVP加密示例及关键注意事项,以确保跨平台加解…

    2025年12月14日
    000
  • Python并发编程:高效获取最快完成任务的结果

    本文详细阐述了在python并发编程中,如何高效地启动多个任务并仅获取其中最快完成任务的结果,同时忽略其他耗时任务。通过引入`concurrent.futures`模块,特别是`threadpoolexecutor`和`as_completed`方法,我们能够以简洁且非阻塞的方式实现这一目标,极大简…

    2025年12月14日
    000

发表回复

登录后才能评论
关注微信