|
|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑1 p1 U" u9 F7 w* [0 z/ Q" c! x
Python中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。
: r. b" R4 J9 p5 l, f
3 }% M& y9 F! L) `% ~" l! e- def main():
( v4 r/ m: L* e$ V - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
6 `* p4 C8 w' O8 F; B - subjs = ['语文', '数学', '英语']! v8 J. x! v$ ~+ E' a
- scores = [[0] * 3] * 5+ r6 }4 g, g8 |8 D( ]% s
- for row, name in enumerate(names):
$ b+ f+ ?# g5 ~5 {3 h0 w3 r6 n - print('请输入%s的成绩' % name)% a& y( G- O5 ^" l' H2 H) q: W
- for col, subj in enumerate(subjs):+ {1 z! W9 Z, O Z$ ^
- scores[row][col] = float(input(subj + ': '))3 ~2 y8 r9 |* G r$ x* i) L7 R
- print(scores)
" ?( d7 o6 p! I* H5 M" v& A$ \8 _ - 1 t K8 d: |3 w0 u3 d
' x+ D& t3 m6 A) M; |! E- if __name__ == '__main__':" m& Z+ I* v4 q. A
- main()
复制代码 S% x3 }1 f/ U8 _
V9 G; q# A) k. [6 I) K; e8 W
我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。
, o# r2 k( u5 M" S* B; Q# O* v
: C4 O" D3 L9 b+ }, p要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。
7 A/ R) g) f. J% }6 j' Y
4 j3 v6 D, V9 Q& z3 o- a = object()
3 U4 q; o7 c) o* h) b - b = ['apple', 'pitaya', 'grape']
复制代码
" ?* z# I- O7 N5 m. A. Y% j
2 w( ?/ A6 B1 n3 Y1 ]知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。- F- M/ o0 b ^3 ]' a! C7 i
# Y% ^2 }" q( s0 X/ d& }
- def main():
) H! o( p0 t2 J7 i# i( a - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
$ N1 B# y: H9 z - subjs = ['语文', '数学', '英语']7 |4 D9 \2 q$ ~3 @. `' n) S8 d
- scores = [[]] * 5
, m' \/ c- {- n2 ~( w/ | - for row, name in enumerate(names):
3 F4 q f! I" I) [$ J5 c0 i - print('请输入%s的成绩' % name)
" U0 M/ T0 p+ D* N7 P3 n - scores[row] = [0] * 3
9 E; E5 Q) `6 V9 | - for col, subj in enumerate(subjs):
8 W0 M+ r) u( v - scores[row][col] = float(input(subj + ': '))
# |( c8 f3 g1 d# k! A! }3 ^& I$ S - print(scores)
* P1 C# g9 y4 S. f" e - , u1 m$ Z; _$ b1 i
; i( D2 l+ _. c- f- if __name__ == '__main__':! c) k4 `. ~$ ?* p* E
- main()
复制代码
# E% W( U) l- f9 b7 g
) V. E0 ^( ~( [6 Z或者
% |2 i/ a. R8 I7 c- v9 v% i% j8 C. L" K' T& n3 ]" r; v' [4 P" @
- def main():
, l; t, o) @) e - names = ['关羽', '张飞', '赵云', '马超', '黄忠']4 \) Q3 o$ O9 {) G+ A2 U" C' a
- subjs = ['语文', '数学', '英语']) f; @1 Q# L8 P
- scores = [[0] * 3 for _ in range(5)]
- G2 r! u* T7 o7 o7 K; e1 q% _ - for row, name in enumerate(names):8 H2 _/ h' [; D( X
- print('请输入%s的成绩' % name)
3 u% _2 v: `; p8 l) i# `, h& D5 b - scores[row] = [0] * 3
3 p- O' G7 u+ g, q - for col, subj in enumerate(subjs):: b/ @$ m" K; z' @2 ~
- scores[row][col] = float(input(subj + ': '))* v" Q- O1 w7 ~- ]
- print(scores)
- W2 [/ ] O/ n4 i9 i - & J K9 V1 S# @
1 W( J7 Y( x3 _, }- if __name__ == '__main__':/ h& y- k* t( B$ l
- main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。
. {2 B0 L" |5 l+ k3 B. m1 c# @" Y& o3 t
( n* q! X7 c j1 Q+ X6 m
- p# ~1 G: v2 g1 O |
|