|
|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑2 \1 o1 a5 Y2 T2 n) H1 n
Python中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。. h* ` }+ v& k9 W" w! s0 H
. z' N/ ~$ F( P2 ^" n- def main():
e, ` A8 l( w, Y4 ~ - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
5 I+ e7 B* G( P' C* c/ n! U - subjs = ['语文', '数学', '英语']
' L+ k1 ]& }/ z ^- d1 z( m5 B0 h - scores = [[0] * 3] * 5
' v6 t) o+ t y - for row, name in enumerate(names):0 H" ^9 G+ R9 T2 ?. G
- print('请输入%s的成绩' % name); Z' { ~9 `9 h, R) e
- for col, subj in enumerate(subjs):( A/ ^. N; s' x& h( G& O1 c' y; u3 k8 ]
- scores[row][col] = float(input(subj + ': '))
( ^7 m$ N, ]# Z - print(scores)
) N/ y/ m% m0 X7 f0 u) V# {
+ u4 \) D- g- v( [' h1 E- % t/ {) s- w1 \0 T) A/ f
- if __name__ == '__main__':$ p* W: ?' W9 w0 @4 U3 D S5 I, \
- main()
复制代码
3 E0 H# D' R; F. V% T2 J' d2 E2 T2 Z' p1 k+ |1 _
我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。
I# L$ q! n+ b, l
. [( F0 u4 ?: K1 U8 c0 ]要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。/ d d* S& L' p1 a$ P( K
, o1 n" d: W% D# o; }1 O. ~
- a = object()+ ~" T" K! H$ O; |' {# i
- b = ['apple', 'pitaya', 'grape']
复制代码 % x8 X! P/ f& a4 Z( d
& d) k' {5 l4 l. O- V# D6 y
知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。
, g6 h+ m( C4 V! F/ k$ l3 x3 I2 j( H. z/ u* t4 m7 d: \$ j
- def main():
D" k0 A7 {& ~( e* E/ P2 y8 u - names = ['关羽', '张飞', '赵云', '马超', '黄忠']
' D! r2 z5 u1 m! C9 `1 Q! P - subjs = ['语文', '数学', '英语']
6 ~. a2 c2 @& w T$ u1 n - scores = [[]] * 5: Z0 ^7 s8 l+ P7 ?3 _
- for row, name in enumerate(names):6 c" K4 d- n$ M! m
- print('请输入%s的成绩' % name)
, x4 ?% X) F! [; |' j( Y - scores[row] = [0] * 3
3 C! I: b2 ]% ?" h+ r$ u4 x. v - for col, subj in enumerate(subjs):( X/ Z2 q, H9 L# @3 G: I% N
- scores[row][col] = float(input(subj + ': '))
( F' A( t' R2 _! C( f - print(scores)
* I: P& h$ H, w2 F
1 y4 }+ i5 J/ M9 v: K- Z, m; k- ( A) ~5 K* G1 U$ M: C7 z
- if __name__ == '__main__':
: W- L6 q% a! r - main()
复制代码
. W# k- V5 z3 l0 r; }3 P; h0 c$ t
: L. H' X9 O v, b- @4 a; y或者
" a: }6 d5 ]# r( T2 L3 c# m* w9 a- e1 Y
- def main():" H& j7 a8 I" \4 q& K
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']# L0 q( D8 l' X' O
- subjs = ['语文', '数学', '英语']
7 H. m: n) ?- o - scores = [[0] * 3 for _ in range(5)]
; i6 B4 K4 F+ q - for row, name in enumerate(names):- f& p, Z$ l3 X/ \# p6 K
- print('请输入%s的成绩' % name)2 U; E" t9 f# Y% h% I2 R( j
- scores[row] = [0] * 3/ N- n; M3 d% W: l/ F% `
- for col, subj in enumerate(subjs):. R) M+ `: J2 X9 F& |
- scores[row][col] = float(input(subj + ': '))
+ B# a; Q; V9 [0 C - print(scores)
& F! i4 q5 i& T; V5 a - , p4 \ `4 s" ^. p* v$ L0 W
- / |0 M: ]; Y+ @; m* o4 j' k* Z
- if __name__ == '__main__':1 b4 [/ W2 K4 e" d
- main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。& F* a' R, O* s( x2 h
) ]6 Q7 E! h8 d9 n" W9 H; r
9 e) ~; G: G3 o5 C& @ |
|