|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑
" a6 _1 L; b8 OPython中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。
1 l/ E6 j [ A) v. }
6 @3 u0 X R) ]3 A- def main():
) J) j, n# B2 M2 {, | - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
9 U# j/ I" `/ ?/ X - subjs = ['语文', '数学', '英语']
/ x+ h: j/ F0 W% c! M - scores = [[0] * 3] * 5
2 J C: n+ D" J/ D% L3 C - for row, name in enumerate(names):* k8 V1 i" j' f3 b( j0 N/ w4 P
- print('请输入%s的成绩' % name)3 {/ h6 W" f* D2 g& C
- for col, subj in enumerate(subjs):/ v; N B3 t2 q0 l
- scores[row][col] = float(input(subj + ': '))& \: h0 |$ o1 Y/ B. ^& D# G# d
- print(scores)
1 M) F: `$ d; ]! P' I- J - 9 ~) \* f' a ~, @5 L1 e5 j& k! P
- % o* F% G9 J8 K8 J/ b& E
- if __name__ == '__main__':6 t! a$ a' m- `) {
- main()
复制代码
_& q; c0 c ]) S# j/ M6 o- B' r B: \% o# N' p+ S
我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。/ g$ d' W k' K9 W D, r J
1 \" p6 m+ V V2 J要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。5 P6 u1 M9 ?. W# r
3 {0 z, s' p5 i% Q, \. P- a = object()
6 A1 g! a$ D7 n& S1 g: X - b = ['apple', 'pitaya', 'grape']
复制代码
8 y( r) v8 L$ X Q" T( Y! w% ~
. N+ Q6 l5 [" A( O+ {! @& D, ^4 p知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。0 w) T+ b$ u) j' C% ]6 [) j
6 `* k/ _4 Z3 M6 t d
- def main():
; D( @+ w3 J$ G3 @) ] - names = ['关羽', '张飞', '赵云', '马超', '黄忠'] |& q; o1 D9 S, S2 A
- subjs = ['语文', '数学', '英语']
2 U- B) C( K' l6 ?2 O. q1 Q2 m2 D - scores = [[]] * 5
: m9 W9 m1 l, s* n3 i. B - for row, name in enumerate(names):0 V. n% X/ K/ p O/ f0 r# K
- print('请输入%s的成绩' % name)6 r7 A2 l) m. d! O7 O% i0 n
- scores[row] = [0] * 3
2 K, [. `7 l, K4 ~! g' F - for col, subj in enumerate(subjs):
2 e; Y" D; s$ G% D( } q - scores[row][col] = float(input(subj + ': '))
8 h. d) H5 d0 x* E% x- f( p | - print(scores)
2 W C* S; y3 h: G0 D, @: c
" ~( \8 Q) S5 |
% n8 Z' z# r$ `" n8 Q% n4 y- if __name__ == '__main__':
; Z3 z+ P2 @. N. i - main()
复制代码 ! b5 U7 r: t* Z3 c/ \/ V" \& r
$ M) J$ P* j+ p! L# j Q或者
$ |3 L& m6 a" M% j' J
" d! G7 w) X ^- B- def main():' t3 i/ x% E$ h0 G/ x ?9 }
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']
) G$ [! X+ _* W% ~2 w6 l- e& c: O - subjs = ['语文', '数学', '英语']" E6 C0 y# ~3 I E6 I
- scores = [[0] * 3 for _ in range(5)] R2 V4 L- s& N6 [( O5 u1 v- o6 L# O
- for row, name in enumerate(names):$ h3 i+ b: L) u& R
- print('请输入%s的成绩' % name)
$ A. O! A0 M* ^7 U4 z - scores[row] = [0] * 33 ?# d! a; B9 F! ~+ \) j, z
- for col, subj in enumerate(subjs):) x2 o$ s; k1 E0 n1 q; O
- scores[row][col] = float(input(subj + ': '))* Y ]- ^2 a T) d
- print(scores)
" ?. p9 d$ Y1 u2 y - 6 K( G X+ q" L% e8 Y# A7 {# Q0 T
- . x+ H; O$ D. t
- if __name__ == '__main__':+ v( I* Z: o* o/ E9 Y( y* t
- main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。
+ k3 i7 d$ }& L$ `3 Y- `" Z
* Z/ f" S% m) L/ L( c f6 s
7 Z# e+ @1 o8 f5 m, o: x
|
|