|
|

楼主 |
发表于 2021-6-1 21:16:14
|
显示全部楼层
坑02 - 嵌套列表的坑
1 o2 i A& D) x3 J; k( |! yPython中有一种内置的数据类型叫列表,它是一种容器,可以用来承载其他的对象(准确的说是其他对象的引用),列表中的对象可以称为列表的元素,很明显我们可以把列表作为列表中的元素,这就是所谓的嵌套列表。嵌套列表可以模拟出现实中的表格、矩阵、2D游戏的地图(如植物大战僵尸的花园)、棋盘(如国际象棋、黑白棋)等。但是在使用嵌套的列表时要小心,否则很可能遭遇非常尴尬的情况,下面是一个小例子。
- W# G2 C, b# \9 v3 [9 k' a: \% t! U$ T& b: p- @; r
- def main():" S( P# b! B0 K2 M A/ Z( R
- names = ['关羽', '张飞', '赵云', '马超', '黄忠']
/ t$ ?/ J& X$ M; A% |- ^ T @ - subjs = ['语文', '数学', '英语']8 J% q5 U/ B+ W% }& _! W' i3 H, Z' P
- scores = [[0] * 3] * 5+ s. {* @4 M0 E, B- Y9 B# C1 t
- for row, name in enumerate(names):. h9 Q- O: z! t( o$ j4 [0 K7 v" I m
- print('请输入%s的成绩' % name)
9 w6 k' t! h6 w% W5 G$ z. F' B - for col, subj in enumerate(subjs):4 v* h' s; k* G. [6 x$ n
- scores[row][col] = float(input(subj + ': '))% m0 o5 B! E4 y7 k
- print(scores)5 e2 }/ V j; E
- 4 l+ T& H7 K ]9 v/ R
2 |: o1 ]2 u* Z. G1 C4 U- if __name__ == '__main__':
& M* I" m! q& j7 X7 B0 Y# p - main()
复制代码
. O6 T* }# o. v7 l9 N
2 x: j0 u7 k( z" i$ t我们希望录入5个学生3门课程的成绩,于是定义了一个有5个元素的列表,而列表中的每个元素又是一个由3个元素构成的列表,这样一个列表的列表刚好跟一个表格是一致的,相当于有5行3列,接下来我们通过嵌套的for-in循环输入每个学生3门课程的成绩。程序执行完成后我们发现,每个学生3门课程的成绩是一模一样的,而且就是最后录入的那个学生的成绩。( Q# o# Z* D# l1 t8 C6 {, K. h/ o
. J/ L# j; D! h8 N9 ]$ S- L
要想把这个坑填平,我们首先要区分对象和对象的引用这两个概念,而要区分这两个概念,还得先说说内存中的栈和堆。我们经常会听人说起“堆栈”这个词,但实际上“堆”和“栈”是两个不同的概念。众所周知,一个程序运行时需要占用一些内存空间来存储数据和代码,那么这些内存从逻辑上又可以做进一步的划分。对底层语言(如C语言)有所了解的程序大都知道,程序中可以使用的内存从逻辑上可以为五个部分,按照地址从高到低依次是:栈(stack)、堆(heap)、数据段(data segment)、只读数据段(static area)和代码段(code segment)。其中,栈用来存储局部、临时变量,以及函数调用时保存现场和恢复现场需要用到的数据,这部分内存在代码块开始执行时自动分配,代码块执行结束时自动释放,通常由编译器自动管理;堆的大小不固定,可以动态的分配和回收,因此如果程序中有大量的数据需要处理,这些数据通常都放在堆上,如果堆空间没有正确的被释放会引发内存泄露的问题,而像Python、Java等编程语言都使用了垃圾回收机制来实现自动化的内存管理(自动回收不再使用的堆空间)。所以下面的代码中,变量a并不是真正的对象,它是对象的引用,相当于记录了对象在堆空间的地址,通过这个地址我们可以访问到对应的对象;同理,变量b是列表容器的引用,它引用了堆空间上的列表容器,而列表容器中并没有保存真正的对象,它保存的也仅仅是对象的引用。/ W9 ~3 G& L" E# y
; n0 m f" t M1 p8 g- a = object()
' g* \$ y! h/ B - b = ['apple', 'pitaya', 'grape']
复制代码 6 P2 x: Y' t. D3 |$ ^; s5 j
! S: y( w, l6 p& H知道了这一点,我们可以回过头看看刚才的程序,我们对列表进行[[0] * 3] * 5操作时,仅仅是将[0, 0, 0]这个列表的地址进行了复制,并没有创建新的列表对象,所以容器中虽然有5个元素,但是这5个元素引用了同一个列表对象,这一点可以通过id函数检查scores[0]和scores[1]的地址得到证实。所以正确的代码应该按照如下的方式进行修改。
2 D3 w1 L1 i+ \' ^6 ^6 ?+ v# d
- g$ \) V) i0 k* D- def main():* M+ z6 F* F \
- names = ['关羽', '张飞', '赵云', '马超', '黄忠'] D; N# ~7 U+ t+ @1 v# O% h
- subjs = ['语文', '数学', '英语']% J0 H1 V. |8 ~$ q0 y) s
- scores = [[]] * 59 \8 H. A0 S+ b& T- K; k3 ?" X
- for row, name in enumerate(names):3 m/ q. t+ m0 P2 k: y( O& ?
- print('请输入%s的成绩' % name)
! x& U# e6 l/ I8 `- D - scores[row] = [0] * 3
6 B" }- W, }! a: b5 I+ i: p u# Y3 f - for col, subj in enumerate(subjs):: i$ z$ i: ^+ v8 c
- scores[row][col] = float(input(subj + ': '))
9 f9 z# @' ?4 g# Z$ `, Z - print(scores)* _$ ^9 c* n# B! o! Q" p' i
$ d- X# U% a! f: A- ; T1 M) t N% b% g" p
- if __name__ == '__main__':
% b# p* |6 @6 m0 Y2 u* V - main()
复制代码
' J4 Q+ J% D. Y+ [8 |- x
! P. l/ k* I e5 i+ v5 e- B* f6 w或者! D: @, _/ ^% `! Q: |$ ~% X
( ^1 w, ]% D0 F5 K- t. ?* {8 c
- def main():
4 l4 x! K3 s! @) @3 ]9 L; w9 G - names = ['关羽', '张飞', '赵云', '马超', '黄忠'], P% t2 J3 {1 O/ s
- subjs = ['语文', '数学', '英语'], H x& N% g- s, l) P
- scores = [[0] * 3 for _ in range(5)]. p$ \# {8 V! P* S1 m
- for row, name in enumerate(names):
2 D# i+ w4 I. P5 A, L - print('请输入%s的成绩' % name)
, o; ]2 ~% ~9 f0 ]5 b - scores[row] = [0] * 3
$ v- U: W* R7 J/ P& O D- j' e* K - for col, subj in enumerate(subjs):% h; m( r7 `/ z0 t; b, [3 u( \ [
- scores[row][col] = float(input(subj + ': '))+ U+ G w# E7 q5 [% D9 e0 O7 H6 a2 A
- print(scores)
# d: Z( P, ?1 a( d$ L" l5 p
# J. C5 L4 U! [1 w( D1 A7 c/ W- - n6 D5 Z: `3 j% }3 B9 R& ]
- if __name__ == '__main__':
8 l1 r; U1 h5 a - main()
复制代码 如果对内存的使用不是很理解,可以看看PythonTutor网站上提供的代码可视化执行功能,通过可视化执行,我们可以看到内存是如何分配的,从而避免在使用嵌套列表或者复制对象时可能遇到的坑。
3 l2 g* {( H1 o8 F
$ d; h, _, ], ?/ L! d3 U
' F% Q5 V8 P9 f; ~ |
|