|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑
o8 q8 }4 ^* [1 n( KPython中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。
' t& c% ]! y7 }. S/ a) X. `- Y( m3 K3 {
- def main():; _6 @ O8 N! D; N4 z3 E |
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']7 Z! F3 @+ p" \1 p1 j$ f
- subjs = ['语文', '数学', '英语']) v& C8 D3 [0 K" ~ W
- scores = [[0] * 3] * 58 O5 [: D' o' \6 w
- for row, name in enumerate(names):
0 K5 D5 @6 q, ^+ a4 N - print('请输入%s的成绩' % name)
0 H, m0 c; Y* w) M) ~7 Q3 {1 m. s - for col, subj in enumerate(subjs):: M6 X \6 R) b) y6 e" u
- scores[row][col] = float(input(subj + ': ')) }2 z( B' i, U! Z. D) l6 h' R( S
- print(scores)
! C6 X; D8 V2 D p
, z% o8 i. A. {/ n+ f; u c- ! O! a0 W1 C o; n- v
- if __name__ == '__main__':% }9 I9 X* h/ \
- main()
复制代码
, W$ B r5 M: C3 Q
- `( t4 W% i. A- S( ~! K8 h# Y2 S, ?* m我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。
/ z& p& t. v. I$ Y. W8 i: O: s% u) z! C% d; L5 i
要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。
9 S2 j* V8 c- c- S1 d* A4 {
4 i5 P6 ^2 y0 M, y' D5 T" V- a = object()
6 t' T* `/ C1 s1 T - b = ['apple', 'pitaya', 'grape']
复制代码
5 H6 w3 U9 E0 P* E6 Q7 b$ Z
, C! v6 F, n1 k: t' e+ v: ?0 \知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。% K, ]6 C% `2 z
. z; d+ [" l. F/ l) z8 [- def main():
( Y2 _# g9 j% C; q - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
; g2 }" \' @: ~0 t2 q - subjs = ['语文', '数学', '英语'], `5 |5 w% F5 s
- scores = [[]] * 5
9 i" F: S! v0 u5 a8 K" V - for row, name in enumerate(names):3 N; ?7 N# c4 H
- print('请输入%s的成绩' % name)/ c% j( ~( y+ X- `- m
- scores[row] = [0] * 3( p, F. d9 Q5 ^+ U: j. t' B
- for col, subj in enumerate(subjs):. h9 U+ A7 T, V+ ~( Q
- scores[row][col] = float(input(subj + ': '))
" h; B, Z1 h# ]3 t6 m4 D. y - print(scores)& b7 n) l9 V, `8 h: c
- 4 N1 A& B0 }4 o
$ y% J4 d E3 I ~! I( F2 }! e- if __name__ == '__main__':
! p4 V: m. O" c U' R( V - main()
复制代码 $ R( A$ d. J% k5 v; O" y
, }0 I, F2 A4 I. l4 _或者
5 {, n- G# G# f, B; J' d, H% V4 b2 C" c) t6 }3 ^
- def main():* v2 l8 p% R) v( i
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']. V' j( V! L( V5 i, f
- subjs = ['语文', '数学', '英语']' D+ l" j! l, U' a3 Y
- scores = [[0] * 3 for _ in range(5)]# C+ D9 B4 P" T2 u3 q# p( q: U
- for row, name in enumerate(names):
+ H. U4 Y; y. i1 p) Z3 u8 I& G - print('请输入%s的成绩' % name)- S' ]8 O0 `5 s8 U$ j; H( T/ T
- scores[row] = [0] * 3* h& j/ B5 F: |# V
- for col, subj in enumerate(subjs):
: u, b7 N) M& Y7 i. T* h/ L1 x - scores[row][col] = float(input(subj + ': '))/ g. z( I6 z5 l$ R
- print(scores)
$ i& i) k* y8 ~0 T
6 k- K7 |9 t0 L4 \+ P2 G
" K7 V `3 K [3 \- if __name__ == '__main__':7 Q2 J; G5 Z& C+ _, a7 ?
- main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。6 C. B Q* h1 o
: n# C5 @9 u3 J+ z
; z4 g. I! Y/ W! Y |
|