Python嵌套列表的引用陷阱与正确初始化方法
本文深入解析Python中嵌套列表的常见陷阱,特别是使用[[0]*3]*5初始化二维列表时导致的引用共享问题。通过区分对象与引用、理解栈与堆内存机制,揭示为何修改一个元素会影响所有行。文章提供了正确的列表推导式和逐行初始化方案,帮助开发者避免此类尴尬错误,写出更健壮的Python代码。
嵌套列表基础
1. 嵌套列表
Python内置了列表(list)这一数据类型,它作为一种容器可以承载其他对象(确切地说是其他对象的引用)。列表中的元素可以是任意类型的对象,包括另一个列表,这种结构被称为嵌套列表。
嵌套列表在实际应用中非常有用,常用于模拟现实世界中的表格、矩阵,或是2D游戏地图(例如《植物大战僵尸》的花园布局)以及棋盘游戏(如国际象棋、黑白棋)的数据结构。
嵌套列表的常见陷阱
2. 识别坑点
使用嵌套列表时需谨慎,否则容易遇到令人尴尬的bug。以下是一个典型示例。
def main(): names = ['关羽', '张飞', '赵云', '马超', '黄忠'] subjs = ['语文', '数学', '英语'] scores = [[0] * 3] * 5 for row, name in enumerate(names): print('请输入%s的成绩' % name) for col, subj in enumerate(subjs): scores[row][col] = float(input(subj + ': ')) print(scores)if __name__ == '__main__': main()
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
subjs = ['语文', '数学', '英语']
scores = [[0] * 3] * 5
for row, name in enumerate(names):
print('请输入%s的成绩' % name)
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
假设我们需要记录5名学生3门课程的成绩,可以定义一个包含5个元素的列表,每个元素又是一个包含3个元素的子列表,这正好对应一个5行3列的表格结构。
随后,我们通过嵌套的for-in循环录入每位学生的3门课程成绩。然而程序运行后发现,所有学生的成绩竟然完全相同,且都等于最后录入的那位学生的成绩。
对象与引用的内存机制
3. 区分两个概念
要解决这个坑,首先必须区分“对象”和“对象的引用”这两个概念。而理解这一区别,需要先从内存中的栈(stack)和堆(heap)说起。
人们常混用“堆栈”一词,但实际上“堆”和“栈”是两个截然不同的概念。程序运行时占用内存空间存储数据和代码,从逻辑上可将这些内存划分为不同区域。
了解底层语言(如C语言)的程序员知道,程序可用的内存从逻辑上可分为五个部分,按地址从高到低依次为:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。
栈用于存储局部变量、临时变量以及函数调用时保存和恢复现场所需的数据。这部分内存在代码块执行开始时自动分配,在代码块结束时自动释放,通常由编译器自动管理。
堆的大小不固定,支持动态分配和回收。若程序需处理大量数据,通常将其存放在堆上。如果堆空间未被正确释放,可能引发内存泄露。Python、Java等语言采用垃圾回收机制自动管理堆空间,回收不再使用的对象。
引用共享示例分析
4. 小例子
因此,在下面的代码中,变量a并非对象本身,而是对象的引用,它记录了对象在堆空间中的地址,通过该地址即可访问对应对象。
a = object() b = ['apple', 'pitaya', 'grape'] b = ['apple', 'pitaya', 'grape']
同理,变量b是列表容器的引用,指向堆空间中的列表容器。列表容器内部并未保存真正的对象,而是保存了这些对象的引用。
基于此,回顾之前的程序,当我们对列表执行[[0]* 3] * 5操作时,仅仅是复制了[0, 0, 0]这个列表对象的地址,并未创建新的列表对象。
因此,虽然容器中有5个元素,但这5个元素都引用了同一个列表对象。这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。读者可以运行以下小例子加深印象。
a = [[0]*3]*5 id(a[0]) id(a[1]) # id相等 id(a[1]) # id相等
正确的初始化方法
5. 正确代码
因此,正确的代码应按照如下方式修改。
def main(): names = ['关羽', '张飞', '赵云', '马超', '黄忠'] subjs = ['语文', '数学', '英语'] scores = [[]] * 5 for row, name in enumerate(names): print('请输入%s的成绩' % name) scores[row] = [0] * 3 #变为不再嵌套 for col, subj in enumerate(subjs): scores[row][col] = float(input(subj + ': ')) print(scores)if __name__ == '__main__': main()'关羽', '张飞', '赵云', '马超', '黄忠']
subjs = ['语文', '数学', '英语']
scores = [[]] * 5
for row, name in enumerate(names):
print('请输入%s的成绩' % name)
scores[row] = [0] * 3 #变为不再嵌套
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
或者采用另一种方式。
def main(): names = ['关羽', '张飞', '赵云', '马超', '黄忠'] subjs = ['语文', '数学', '英语'] scores = [[0] * 3 for _ in range(5)] for row, name in enumerate(names): print('请输入%s的成绩' % name) scores[row] = [0] * 3 for col, subj in enumerate(subjs): scores[row][col] = float(input(subj + ': ')) print(scores)if __name__ == '__main__': main()
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
subjs = ['语文', '数学', '英语']
scores = [[0] * 3 for _ in range(5)]
for row, name in enumerate(names):
print('请输入%s的成绩' % name)
scores[row] = [0] * 3
for col, subj in enumerate(subjs):
scores[row][col] = float(input(subj + ': '))
print(scores)
if __name__ == '__main__':
main()
以上介绍了使用嵌套列表时需要注意的问题及解决措施,希望大家多多总结,从而避免在使用嵌套列表或复制对象时遇到类似的坑。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









