|
|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑
4 _) o5 U. M( V U; zPython中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。
1 ?2 K: Y! Q, Y6 `( P! y
' ^( ]0 T. r6 K; J8 G; e- def main():2 _0 R" U) {1 G Y9 E) a4 ?0 O
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']% [5 |& Z0 ]! o- l+ ? s5 M
- subjs = ['语文', '数学', '英语']
$ v i* U, `3 I/ t% ] - scores = [[0] * 3] * 5
; B5 ~+ ^1 @) m/ \2 U4 z _8 H3 n% v - for row, name in enumerate(names):9 @% Z" O/ W/ p& ~1 X; D) B
- print('请输入%s的成绩' % name): N1 _' z' C& w
- for col, subj in enumerate(subjs):
) H2 u" K1 x3 T/ d7 K ^ - scores[row][col] = float(input(subj + ': '))
' c, A. F' F! p f; y2 _5 e3 E - print(scores)" s6 K: u) T- `6 C! |+ g( G" x
- 7 j' i n1 n4 f
" |* M3 J7 ~7 i- if __name__ == '__main__':( h. Z+ H9 B, H m' J, w
- main()
复制代码 " v$ W/ ^9 v- U' F! P* c1 l! l
; o' Q7 N8 y5 d我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。
; a- o J7 |2 D# I
" R5 { G- |0 u D要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。
; ^9 b7 o% E% e k8 m, d
8 P) f' [0 y) [ j! z) B# n- a = object()% [! l% D& t9 @6 F- J% ?$ d& V4 s
- b = ['apple', 'pitaya', 'grape']
复制代码 . f5 @) q' ~1 q4 ^
( X' a- \- y; t. q
知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。
' f" m# }1 W4 D4 {& b! G" L1 h
^+ w3 j) w- B7 o- def main():
" f, J. X d' @: P- B' c! W6 H - names = ['关羽', '张飞', '赵云', '马超', '黄忠']# P" P9 z6 \" j; q( L& M. h1 J
- subjs = ['语文', '数学', '英语']
- V9 T( r; I+ P4 t$ p; X- p - scores = [[]] * 5
0 H7 z& Q, y8 Q1 s6 `5 t+ k( [ - for row, name in enumerate(names):
/ P/ B# l$ `) h8 @4 Y9 g$ m S - print('请输入%s的成绩' % name)) s8 x9 E; R: v9 r6 }
- scores[row] = [0] * 3
/ L6 J T* b ?) E7 _ - for col, subj in enumerate(subjs):
+ E0 f: y. Y; H) t5 q* s - scores[row][col] = float(input(subj + ': '))
2 K; l" b7 U! n$ S! a; d! X - print(scores)3 Y/ G7 J3 F( B: Y: J# x0 W# v! l' f
- . l! j7 }( ]5 Z k; \% ~7 Z0 B8 [( \
& K* q) {( t& c/ s# ~9 }; }- if __name__ == '__main__':5 L. G5 P( O7 k3 A1 L
- main()
复制代码
2 g7 V0 k% g, l/ Q
, h: b* j- w. s; S. \9 z# Z% s或者2 G; ?+ X: n, Z) n) P1 R% s. M
1 Q+ T2 d2 E0 l# U- ?- def main():+ S& Y7 O, E6 e7 J+ x2 }- ?# Z
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']. }8 s h$ C1 e) {: t
- subjs = ['语文', '数学', '英语']
3 N* I, `: {# S+ B# z - scores = [[0] * 3 for _ in range(5)]
1 q, W( M5 @3 @( J2 R) v0 g( x - for row, name in enumerate(names):
# F; x4 ^; m' O" w - print('请输入%s的成绩' % name)1 q& U! ^' @/ K! h% \- x7 W' ~
- scores[row] = [0] * 3! `' C, r: O9 K. _# C
- for col, subj in enumerate(subjs):% I- K, e" G6 y$ N( G6 B; g) h
- scores[row][col] = float(input(subj + ': '))
8 z3 }+ |8 `/ ?2 A - print(scores)
) K2 F5 J- |2 }# f) y7 z( M: ^) F
/ K& W& e+ f! J: f" u% ^- + f% Z. D1 o0 S% b& E. b
- if __name__ == '__main__':
% ^' k: N ^) |. E O( L" @5 A! w% D; @ - main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。) `; D( I% f% l0 _$ [
2 B4 W5 W! W" h3 J! j7 l
% m- x6 v' P$ u [. S
|
|