|
|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑8 _& R' _/ F1 L0 @9 ]5 h- S7 _
Python中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。' [7 Z- ?% |5 y G+ \( e
* U+ f- U1 K4 E% K7 ~, M) Q- def main():% G# Y( i, I' u4 z+ @
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']
3 l3 [7 W3 W! |+ W) b7 w f - subjs = ['语文', '数学', '英语']
# n4 m6 e, u9 l$ E - scores = [[0] * 3] * 5
$ N7 z( K, P9 [$ D - for row, name in enumerate(names):
: y& y* I+ _% [1 i - print('请输入%s的成绩' % name)
; L8 L) D' y% I; k n# Q - for col, subj in enumerate(subjs):
; x) g3 e! j( O ~; h/ G' C# [* a } - scores[row][col] = float(input(subj + ': '))
4 B# s: h. R+ Q, p# M$ N - print(scores)* D5 `1 [9 D; l9 \2 L
. \' ^, h& S9 u+ M1 y9 C
- E% O9 X! i) t1 d: L) j1 q; A- if __name__ == '__main__':
- Y( h) l5 d) A: L - main()
复制代码 ; \" e6 s0 s* r
& Y4 h {3 P% w7 R+ b' j( d我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。4 B1 L6 p7 S( z+ m% b# @1 P
8 Q+ `; E5 l# l% [4 R要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。% Y! N: @$ @1 _4 g) h. X5 N! N( z" W1 M
; z' M. p- t, r2 d5 a& M
- a = object()& Y5 ^% s- }+ w$ Z, e8 ]
- b = ['apple', 'pitaya', 'grape']
复制代码 8 T4 X5 J& u- f8 T4 {) s. ?
9 E7 @$ M" O# R1 ^" M$ k5 p P9 s
知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。
3 o/ x! x7 R' D. ~
- i6 Z9 C# [! r( }+ z; j9 ?0 i1 a1 ~- def main():
# T' J+ s1 b6 Y" v: y9 u! x - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
/ W# Y$ @. G) N. x( _& s8 c - subjs = ['语文', '数学', '英语']7 R4 F1 H" r, ], Y! I/ _& _
- scores = [[]] * 5
" [5 W& }; c6 D. p - for row, name in enumerate(names):
+ x1 i0 ~4 v S S2 t: S - print('请输入%s的成绩' % name)" v. b1 f# `+ R; k1 d7 M
- scores[row] = [0] * 3
j) U3 P& A$ X& }, M - for col, subj in enumerate(subjs):. n2 F& e: w/ `& i* |
- scores[row][col] = float(input(subj + ': '))' b( Z8 a& L1 d& o
- print(scores)7 Y5 k) R. T9 d6 B4 U h
- 6 w' y6 E" D+ q1 O& ?3 G4 M: }0 K9 x
- 5 P1 j! b7 i6 G
- if __name__ == '__main__':
: O2 ]3 A1 ]$ s* p0 A - main()
复制代码 & X, @5 U* w3 p+ a _: B
9 P1 Z% n) P# Y或者0 ^/ @4 L5 ]8 n& {0 R$ ]0 p
) V% E6 D p) p6 x- V7 L! n
- def main():
' a) e8 O2 e, _6 A" x - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
. Z5 f K5 B/ c# S( @/ y! |9 n1 p- \ - subjs = ['语文', '数学', '英语']
r! E h& K6 E+ [' N1 q7 g - scores = [[0] * 3 for _ in range(5)]' v0 X8 }1 k, q. W8 V# D' [
- for row, name in enumerate(names):0 e5 }! ^/ `9 I8 M+ W
- print('请输入%s的成绩' % name)( I7 J. c) X( W& x( b
- scores[row] = [0] * 3
+ l& C% _5 X. f6 v: Z# r - for col, subj in enumerate(subjs):! Y3 p3 S0 a% ~& p, W
- scores[row][col] = float(input(subj + ': '))
. U: P0 d$ B% E5 f1 [. f - print(scores)
: G8 e4 p6 x% [0 V - 9 V- l6 ^/ P) m1 Y# D) j
- ; f: B. \1 H. o" j& |' F* d
- if __name__ == '__main__':' s: v. T( V# Y# `1 ^8 U2 {$ Z
- main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。
+ O, x7 _6 {) H. z0 P+ D. s
" h. ^8 o; A+ t: `9 S$ A7 P( \
Z! P# H4 [. c& X0 j
|
|