0

0

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

碧海醫心

碧海醫心

发布时间:2025-10-03 13:47:21

|

462人浏览过

|

来源于php中文网

原创

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

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

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

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

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

# 假设 A 是一个用于确定维度的列表,例如 A = [[0,0],[0,0],[0,0]]
# 这里的 A 仅用于获取维度,实际内容不影响示例
rows = 3
cols = 2

empty_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 维度不变,假设仍为 3x2
rows = 3
cols = 2

for 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)所赋的值。

Bandy AI
Bandy AI

全球领先的电商设计Agent

下载

例如,当 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, 

你会发现:

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

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

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

rows = 3
cols = 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 + j

print("\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的引用机制和赋值操作的本质,可以有效避免在处理复杂数据结构时遇到的常见陷阱,编写出更健壮、可预测的代码。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1501

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

232

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

87

2025.10.17

sort排序函数用法
sort排序函数用法

sort排序函数的用法:1、对列表进行排序,默认情况下,sort函数按升序排序,因此最终输出的结果是按从小到大的顺序排列的;2、对元组进行排序,默认情况下,sort函数按元素的大小进行排序,因此最终输出的结果是按从小到大的顺序排列的;3、对字典进行排序,由于字典是无序的,因此排序后的结果仍然是原来的字典,使用一个lambda表达式作为key参数的值,用于指定排序的依据。

391

2023.09.04

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1501

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

624

2023.11.24

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.3万人学习

Django 教程
Django 教程

共28课时 | 3.6万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号