CS1602计算导论

第 5 讲 · 课件

列表

Introduction to Computation (CS1602) · Lecture 5

Lists

Instructor: Tao Huang
Part II: Built-in Data Structures · Fall 2026
1

全部讲稿

展开
  1. 第 1 页 · Lists

    这一讲讲列表。它是 Python 里最常用的容器,也是这门课遇到的第一个可以原地修改的类型。

    第 3 讲说过,几个名字可以共用同一个对象;对整数和字符串,这不会带来问题,因为它们不能修改。列表可以修改,于是会出现一些初看很意外的现象。这一讲的后半部分专门讲这些现象。

  2. 第 2 页 · Built-in Data Structures

    第一部分讲的是单个的值,以及怎么用分支和循环处理它们。

    第二部分讲怎么把一批值放在一起,当作一个整体来处理,以及遇到具体问题时,该选哪一种容器。一共四讲,今天是第一讲。

  3. 第 3 页 · Where Lecture 4 Ended

    到上一讲为止,已经能写出包含输入、判断、循环和函数的完整程序,但处理的都是单个的值:一个整数、一个字符串、一个布尔值。

    从这一讲起,处理的是一批值。要考虑的问题也随之变多:怎么存、怎么取、怎么遍历,以及修改一批数据时会发生什么。

    第 3 讲讲过:Python 的变量是引用,赋值复制的是地址;整数和字符串不可变,所以几个名字共用一个对象不会出问题。今天的列表是可变的,这两条放在一起,就是今天后半部分所有现象的来源。

  4. 第 4 页 · A Shared Shopping List

    先说一件和代码无关的事。

    两个室友共享一份购物清单。一个人在自己手机上加了「酱油」,另一个人什么都没做,打开一看,清单上也多了酱油。

    这一点也不奇怪:两个人看的是同一份清单,各自手机上的只是一个入口,不是各自的副本。

    同样的事情在 Python 里发生时,很多人会觉得莫名其妙。下一页就是这样一个例子。

  5. 第 5 页 · An Opening Prediction

    两段代码的结构一模一样:定义一个东西,把它赋给第二个名字,通过第二个名字做修改,再打印第一个名字。

    左边是整数 x,右边是列表 nums。append 的意思是在列表末尾加一项,今天会讲。

    左边打印 1,这一点第 3 讲已经讲过。右边打印什么?这一页不给答案,请先定一个判断,讲完别名那一节再回来对照。

  6. 第 6 页 · 随堂小测 · 第 1 轮

    开场先收一道题,不公布答案。讲完别名与拷贝,再把这一轮的分布放出来对照。

    现在选错没有关系。这道题的作用是先形成一个判断,有了判断,后面的解释才有对照。

  7. 第 7 页 · Today's Question

    这一讲从头到尾在回答一个问题:只动了 b,为什么 a 也变了?

    答案和那份共享购物清单是同一回事,只是发生在代码里。在回答之前,要先把列表本身讲清楚:怎么建、怎么取、怎么改、怎么切。

  8. 第 8 页 · Why a Container

    到目前为止,每个变量都只存一个值。可实际的数据往往是一批:一个班的成绩、一个文件里的所有行。这一节看看只用单个变量处理一批数据时会遇到什么问题。

  9. 第 9 页 · Five Scores, Five Variables

    先看没有列表的时候怎么写。

    五个成绩,五个变量:score1 到 score5。要求平均分,就把五个加起来除以五。代码能运行,逻辑也没错。

    变量个数很少、而且固定不变的时候,这样写可以接受。问题在下一页。

  10. 第 10 页 · When the Requirements Change

    需求变了,而且是很自然的变化。

    如果是五十个成绩,就要写五十个变量名,加法要写五十项。要求最高分,得写一长串比较;要按分数排序,用五十个独立的变量几乎做不到。

    更关键的是,数量往往在写代码时还不知道。文件里有多少行、用户会输入多少个数,要等程序运行起来才清楚,而变量名必须在写代码时就定下来。

    所以需要一种新的东西:能存放一批值,数量可以等到运行时才确定。

  11. 第 11 页 · The List(列表)

    这个东西就是列表。

    一个名字管一批值,多少个都可以。len 求个数,sum 求和,max 求最大值,平均分就是 sum 除以 len。

    和上一页对比:五十个成绩和五个成绩,这段代码一个字都不用改。

  12. 第 12 页 · Data Structures(数据结构)

    列表是一种数据结构。这个词在这门课里会反复出现,意思是:一种组织数据的方式,加上针对它的一组操作。

    组织方式决定了哪些操作快、哪些操作慢。所以很多时候,选对数据结构比优化代码更能解决问题。第二部分的四讲讲四种常用的数据结构,以及它们各自适合的场合。

  13. 第 13 页 · Building and Reading a List

    先讲最基本的两件事:怎么写出一个列表,怎么从里面取出某一项。

  14. 第 14 页 · List Literals

    列表的写法是方括号,各项之间用逗号隔开。

    里面可以是整数、浮点数、字符串,也可以是另一个列表。空列表就是一对空方括号。

    最后一项后面多写一个逗号,Python 也接受。列表分多行写的时候这很方便:加一项只需要加一行,不用改上一行。

  15. 第 15 页 · type() and len() of a List

    不管里面放的是什么,列表本身的类型都是 list。

    type 给出类型,len 给出有几项。注意最后一个:nested 里有一个列表,但 len 是 3。里面那个列表整个算一项,不会被展开。

    处理嵌套结构时,这一点经常被弄错。

  16. 第 16 页 · The list() Constructor(构造函数)

    除了直接写方括号,还可以用 list() 来构造列表。

    list() 不带参数得到空列表。给它一个字符串,会把字符串拆成一个个字符,因为字符串可以逐个取出字符。给它一个 range,会把 range 展开成列表,第 4 讲打印 range 时用的就是这个办法。

    能逐个取出元素的东西,统称可迭代对象。for 后面的 in 能接的,也正是这一类东西。

  17. 第 17 页 · Indexing from 0

    取第几项用方括号加下标,和第 3 讲取字符串里的字符一样。下标从 0 开始。

    第一项是 a[0],第二项是 a[1],最后一项是 a[len(a) - 1]。下标表示的是「离开头有多远」:第一项离开头 0 个位置。这一节的第四部分会看到,这样编号在计算地址时最方便。

    从 0 开始是初学时最容易出错的地方。凡是涉及「第几个」的循环,写完先用一个三四项的小列表试一遍边界。

  18. 第 18 页 · Negative Indices

    负数下标从末尾往前数。a[-1] 是最后一项,a[-2] 是倒数第二项。

    这个写法很常用,因为取最后一项不需要先知道长度。a[len(a) - 1] 和 a[-1] 结果相同,后者更短,也更不容易写错。

    负数下标从 -1 开始。没有 -0,因为 -0 就是 0,那是第一项。

  19. 第 19 页 · IndexError

    下标越界会报 IndexError。

    一个有 4 项的列表,合法的下标是 0 到 3,以及 -1 到 -4。写 a[4] 就会出错,报错信息是 list index out of range。

    报错其实是好事:程序在出错的地方立即停下,而不是拿一个错误的值继续往下算。记住这一点,因为后面的切片不是这样:切片越界不报错。

  20. 第 20 页 · Membership: in and not in

    in 判断一个值在不在列表里,not in 反过来。结果是 True 或 False。

    它比自己写循环逐个比对简洁得多,意思也更清楚。

    最后一行:[1, 2] 不是 [1, 2, 3] 里的某一项,所以是 False。in 问的是「有没有一项等于它」,不是「有没有一段等于它」。

    对列表用 in,要从头到尾逐项比较,列表越长越慢。第 6 讲会讲集合,集合的 in 几乎不受长度影响。

  21. 第 21 页 · Walking Through a List

    第 4 讲的 for 循环,在列表上用得最多。这一节讲遍历列表的几种写法。

  22. 第 22 页 · for x in a

    遍历列表最直接的写法是 for x in a。

    每一轮循环,x 依次取到列表里的一项。x 拿到的是元素本身,不是下标。这和 C、Java 里习惯的「用下标循环」不一样。

    只需要每一项的值时,这是最好的写法:短、清楚、不会越界。

  23. 第 23 页 · Two Ways to Write One Loop

    同一个循环的两种写法。

    左边用下标:自己初始化 i、自己判断边界、自己加一,再用 fruit[i] 取值。右边直接遍历元素。结果一样,右边更短,也不可能越界。

    什么时候需要知道下标?需要知道「这是第几项」的时候。即使那样,也有更好的写法,见下一页。

  24. 第 24 页 · enumerate()

    既要元素又要下标时,用 enumerate。

    它每一轮给出两个值:下标和元素。所以循环变量写成 i, x,这是第 3 讲讲过的同步赋值。

    和 range(len(a)) 再取 a[i] 的写法相比,enumerate 更短,也不会出现下标和元素对不上的问题。

    想让编号从 1 开始,写 enumerate(a, 1)。打印给人看的清单时常用。

  25. 第 25 页 · What a List Really Is

    列表里可以放任何类型的值,而且不管列表多长,取第几项都一样快。这两点放在一起其实并不简单。这一节讲列表在内存里是什么样子,它是后半节课所有现象的根源。

  26. 第 26 页 · Arrays(数组)

    先说 C、Java 里的数组。数组是一段连续的内存,每一项类型相同、占的字节数相同。

    所以第 i 项的地址可以直接算出来:起始地址加上每项的大小乘以 i。一次乘法一次加法,不管 i 是多少,都一样快。

    这也解释了下标为什么从 0 开始:第 0 项的地址就是起始地址,不用再减 1。

    但 Python 的列表可以混放不同类型的值,下一页看看这会带来什么问题。

  27. 第 27 页 · A Python List Holds Anything

    Python 的列表可以放任何东西,同一个列表里可以混着放整数、字符串、浮点数,甚至另一个列表。

    这些值占的字节数各不相同。第 2 讲讲过,一个小整数对象就要 28 个字节,一个长字符串可以占几千个字节。大小不一样,上一页那个算地址的公式就用不上了。

    那 Python 怎么还能按下标快速取到第几项?答案在下一页。

  28. 第 28 页 · A List Stores Addresses

    关键在于:列表里存的不是数据本身,而是数据的地址。

    第 3 讲讲过,Python 的名字里存的是对象的地址。列表的每一格也一样:格子里放的是一个地址,指向真正的对象。

    不管对象是一个小整数还是一个很长的字符串,地址的大小都是固定的。所以格子大小统一,按下标取值仍然很快;同时元素类型可以随意,因为真正的数据放在别处。

    这个设计解释了三件事:列表为什么能混放各种类型;两个名字为什么能指向同一个列表;复制列表时,里面那一层为什么不会跟着复制。后两件马上就会遇到。

  29. 第 29 页 · Nesting Without Limit

    既然格子里存的是地址,这个地址也可以指向另一个列表,那个列表的格子又可以指向下一个列表,一层套一层。

    deep 套了六层,最里面是 1。要取到它,得连写六次 [0]。而 len(deep) 是 1:最外层只有一项,就是指向第二层的那个地址。

    实际写程序时很少嵌套这么深。两层的嵌套很常见,比如用列表的列表表示一张表格。

  30. 第 30 页 · id() on a List

    第 3 讲用过 id():它返回一个对象的地址。

    a 和 a[0] 的 id 不同:列表是一个对象,它的第 0 项是另一个对象,列表的格子里存着第 0 项的地址。

    b = a 之后,b 和 a 的 id 相同,a is b 是 True:两个名字指向同一个列表。这一点后面会反复用到。

    接下来几页都用 id 来验证「是不是同一个对象」,而不是靠猜。

  31. 第 31 页 · Changing a List

    第 3 讲讲过,整数和字符串不可变,每次「修改」都是造一个新对象。列表不一样,它可以在原地修改。这一节讲怎么修改,以及修改时要注意什么。

  32. 第 32 页 · Lists Are Mutable(可变)

    列表是可变的:对象还是那一个,内容可以修改。

    看输出:修改前后,列表的内容变了,id 没变。还是同一个列表,只是第 0 项换了。

    字符串正好相反。第 3 讲试过,s[0] = "H" 会直接报错,所有「修改」字符串的操作,实际上都在造一个新字符串。

  33. 第 33 页 · append()

    append 在末尾加一项。

    它加的是「一项」:传进去一个列表,这个列表整个作为一项放进去,而不是把里面的元素逐个加进去。所以最后 len 是 5。

    append 是在原来的列表上修改,不造新列表。这一点让它和后面的加号有本质区别。

  34. 第 34 页 · append, extend and +

    三种「加东西」的方式放在一起比较,这是这一节的重点。

    append 把参数整个作为一项加进去。extend 把参数里的每一项分别加进去,所以传一个两项的列表会多出两项。加号造一个新列表,原来的两个都不变。

    append 和 extend 修改原列表,加号不修改。选哪一个,取决于要不要保留原来的列表。

  35. 第 35 页 · In Place or a New List

    这一页用 id 验证上一页的说法。

    extend 之后,a 的 id 不变,说明是在原列表上加的。a = a + [5, 6] 之后,id 变了:加号造了一个新列表,a 改指到了新列表上。

    这个区别在函数里特别要紧。把一个列表传进函数,函数里用 extend 或 append,调用者手里的那个列表也会跟着变,因为两边指向的是同一个列表;用加号再赋值则不会。

  36. 第 36 页 · In-Place Methods Return None

    原地修改列表的方法都返回 None。

    append、extend、sort、reverse、insert、remove 都是如此。sort 是排序,reverse 是反转,insert 是在指定位置插入。它们的工作是修改原来的列表,没有新的东西要交回来,就统一返回 None。

    这是有意的设计:返回 None 等于告诉调用者,这是一个原地操作。

  37. 第 37 页 · The a = a.append(x) Bug

    上一页那条规则,最常见的出错形式是 a = a.append(x)。

    写的人以为是「把 x 加进去,再把结果赋回给 a」。实际发生的是:append 先把 x 加进列表,这一步是对的;然后返回 None,None 又被赋给了 a。于是 a 变成了 None,原来的列表反而找不到了。

    这个错误不会当场报错,要等到下一次用 a 时才报错,报错信息还指向别的地方。出错的位置和报错的位置不一样,这类问题最难查。

    正确写法就是 a.append(x),不要赋值。

  38. 第 38 页 · The In-Place Family

    原地修改的方法可以归成一类,记住它们的共同点比逐个记更有用。

    append 加一项,extend 加一批,insert 在指定位置插入,remove 按值删除第一个匹配项,sort 排序,reverse 反转,clear 清空。它们都修改原列表、都返回 None。

    pop 是例外:它按下标取出一项并删除,然后把取出的那一项返回,因为这个值确实有用。

    如果想要一个排好序的新列表、不动原来的,用 sorted(a);想要一个反转的新列表,用 a[::-1],下一节讲。

  39. 第 39 页 · Slicing(切片)

    第 3 讲讲过字符串的切片。列表的切片规则完全一样,另外列表是可变的,所以切片还可以放在等号左边,用来修改一段内容。

  40. 第 40 页 · Half-Open Slices

    a[i:j] 取从第 i 项到第 j 项,但不包含第 j 项。

    这叫左闭右开,和第 3 讲字符串切片、第 4 讲的 range 是同一个约定。好处有两个:切出来的长度正好是 j 减 i;a[:i] 和 a[i:] 正好把列表分成两段,不重复也不遗漏。

    开头的 0 可以省略,结尾省略表示到末尾,两个都省略就是整个列表。

  41. 第 41 页 · The Step

    再加一个冒号可以指定步长:a[start:stop:step]。

    a[::2] 每隔一项取一项,a[1::2] 从第二项开始每隔一项取一项。

    a[::-1] 步长为负,得到倒过来的列表。这个写法很常见。它造的是一个新列表,和上一节的 a.reverse() 不同:后者原地反转,返回 None。

  42. 第 42 页 · An Interactive Slicer

    这一页可以动手操作。拖动下面三个游标,亮起来的格子就是切出来的部分,右边显示结果。

    先把 stop 拖到 4,看第 4 格亮不亮。它不亮,这就是左闭右开。

    再把 stop 拖到 9。这个列表只有 6 项,切到 9 却不报错,只给出到末尾为止的部分。下一页讲这一点。

    最后把 step 拖到 -1,看看结果。

    导出的 PDF 上是静态的一帧,要拖动请到课程网站的课件页。

  43. 第 43 页 · Slices Never Raise IndexError

    切片越界不报错。

    前面讲过 a[4] 这样的下标越界会报 IndexError,但 a[1:99] 不会,它只返回从第 1 项到末尾的部分。a[99:] 也不报错,返回一个空列表。

    这在很多场合很方便。比如取前十项,不足十项就有多少取多少,直接写 a[:10],不用先判断长度。

    但这也意味着切片写错了不会当场发现:拿到的是一个空列表或少几项的结果,要到后面才发现不对。不报错不等于正确。

  44. 第 44 页 · Slice Assignment

    列表的切片不但能读,还能赋值。字符串不可变,就不能这样做。

    a[1:3] = [20, 30] 把第 1、2 项换成新的两项。两边的长度不必相等:a[1:3] = [7, 7, 7, 7] 用四项替换两项,列表变长。赋一个空列表,就是删掉那一段。

    右边必须是可迭代对象。a[1:3] = 20 会报错,因为 20 不能逐个取出元素。

  45. 第 45 页 · A Slice Is a New List

    切片总是造一个新列表。

    b = a 只是多了一个名字,a is b 是 True。c = a[:] 是切片,得到一个新列表,a is c 是 False;但内容相同,a == c 是 True。

    所以 a[:] 是一个常见的写法,意思是把整个列表切一份,得到内容相同、但互相独立的新列表。后面讲拷贝时会用到。

  46. 第 46 页 · Copying by Slice Is Fast

    复制两百万项的列表,一种写法是自己写循环逐项 append,另一种是切片。perf_counter 返回当前时间,前后相减就是这段代码用了多少秒。

    右边的数字是构建课件时实际测到的,每台机器不一样,但切片总是快很多。切片在 Python 内部用 C 代码一次完成;自己写的循环每一轮都要由解释器执行一次 append。

    要注意的是,这只是常数倍的差别。两种写法处理 n 项,所花的时间都和 n 成正比。要让程序快上一个数量级,得换算法,而不是换写法。第 11 讲讲这个问题。

  47. 第 47 页 · Two Names, One List

    第 3 讲讲过,b = a 复制的是地址,两个名字共用一个对象;整数和字符串不能修改,所以共用不会带来问题。

    列表可以修改。两个名字共用一个列表时,通过一个名字修改,另一个名字就会看到。这一节回答开场的问题。

  48. 第 48 页 · The Opening Question Revisited

    回到开场那道题,可以先把开课时收上来的分布放出来。

    nums2 = nums 复制的是地址,不是列表。赋值只是让第二个名字指向同一个列表,没有产生新的列表。所以通过 nums2 做的 append,从 nums 也能看到。

    左边为什么不变?x2 = x2 + 1 并没有修改那个 1,而是另造了一个 2,再让 x2 改指过去。x 还指着原来的 1。

    这就是那份共享购物清单:两个入口,一份清单。

  49. 第 49 页 · b = a Copies the Address

    用代码验证:b = a 之后,a is b 是 True,两个名字指向同一个列表。

    所以 b.append(4) 之后,a 也变成了四项,因为自始至终只有一个列表。

    同一个对象有两个名字,这种情况叫别名。

  50. 第 50 页 · One List, Two Names

    画成图更清楚:两个名字,两个箭头,指向同一个列表。

    b = a 做的事情是「让 b 也指向 a 所指的对象」,而不是「把 a 的内容抄一份给 b」。

    为什么这样设计?第 3 讲的取舍表讲过:赋值只复制一个地址,不管列表有一百万项还是一项,代价都一样小。

    这个规则对后面所有可变类型都成立:字典、集合、以及第 9 讲自己定义的类。

  51. 第 51 页 · Mutating vs Rebinding

    这里要区分两种容易混淆的操作:修改和重新绑定。

    b.append(4) 是修改:改的是列表本身。a 和 b 指的还是同一个对象,所以 a 能看到这个改动。

    b = [9, 9] 是重新绑定:它没有动原来的列表,只是让 b 改指一个新列表。a 仍然指着原来那个,所以不受影响。

    判断方法:看等号左边。左边只是一个名字,就是重新绑定;左边带方括号,或者调用的是方法,就是修改。

  52. 第 52 页 · Getting Your Own Copy

    想要一个独立的副本,有三种等价的写法:a[:]、a.copy()、list(a)。

    三种都造一个新列表,之后修改其中一个不影响另一个。a.copy() 的意思最明显。

    但下一页会看到,这种复制只解决了一半问题。

  53. 第 53 页 · A Copy Goes Only One Level Deep

    a 是一个嵌套列表,里面有两个小列表。b = a.copy() 之后,修改 b[0][0],a[0][0] 也变了。

    最后一行 a[0] is b[0] 是 True:a 和 b 的第 0 项是同一个小列表。

    这种只复制最外层的做法叫浅拷贝。为什么会这样,下一页画出来。

  54. 第 54 页 · What a Shallow Copy Shares

    回忆这一讲第四部分:列表的格子里存的是地址。

    a.copy() 造了一个新的外层列表,也就是图中 b 的外层。它把 a 外层格子里的地址原样抄了过去。抄过去的是地址,所以两个外层的格子,指向的是同一组内层列表。

    b[0][0] = 99 先通过 b 的第 0 格找到第一个内层列表,再修改它的第 0 项。这个内层列表 a 也在用,所以从 a 也看得到 99。

    这是这一讲最容易出错的地方,也最难自己发现:程序不报错,只是给出一个没有预料到的结果。

  55. 第 55 页 · Deep Copy(深拷贝)

    要连内层一起复制,用 copy 模块里的 deepcopy。

    深拷贝沿着每一个地址往下,把每一层都复制一份,得到的新结构和原来完全独立,改哪一层都互不影响。

    代价是慢,嵌套越深越慢。所以不要一律用 deepcopy:列表里只放数、字符串这类不可变的值时,浅拷贝就够了。

  56. 第 56 页 · copy vs deepcopy

    用代码验证:修改 a 里面那个小列表之后,shallow 跟着变了,deep 没有变。

    三行输出放在一起,区别一眼就能看出来。遇到「明明只改了一个,却两个都变了」的情况,就回来看这一页。

  57. 第 57 页 · A Grid Built With `*`

    这是嵌套列表最典型的一个错误:用乘法造二维表格。

    [[0] * 3] * 3 看起来是三行三列的 0,打印出来也确实如此。但把 grid[0][0] 改成 1 之后,三行的第一项全变成了 1。

    原因和浅拷贝一样:外层的乘法把同一个内层列表的地址重复了三次,三行其实是同一行。

    正确的做法是在循环里每次新造一行,这样三行是三个不同的列表。循环变量写成下划线,表示这个变量用不到。学完列表推导后,这三行还可以写得更短。

    内层的 [0] * 3 为什么没问题?因为 0 是整数,不可变,共用同一个 0 不会有任何影响。问题只出在可变对象上。

  58. 第 58 页 · Never Default to a Mutable Object(可变默认值)

    第 4 讲讲了默认参数,当时说还有一个问题要等讲完列表再讲,就是这一页。

    左边第二次调用 add_item(2),本来应该得到 [2],结果却是 [1, 2],带着上一次调用留下的 1。

    原因在于默认值什么时候创建。那个空列表是在执行 def 那一行时创建的,整个程序里只创建一次。之后每次调用不传 box,box 指向的都是同一个列表,上一次 append 进去的东西还在。这又是两个名字共用一个可变对象。

    右边是正确写法:用 None 当默认值,进入函数后判断,再新造一个空列表。这样每次调用得到的都是新列表。

    结论:默认值只用不可变的东西,比如数、字符串、None。

  59. 第 59 页 · Three Minutes with Your Neighbour

    花三分钟和旁边的同学讨论这个问题。

    把想法说出来,比自己默想更容易发现哪里没想清楚。

    参考答案有两种:先用 a.copy() 复制一份再对副本调用 sort;或者直接用 sorted(a),它返回一个排好序的新列表,不动原来的。

  60. 第 60 页 · List Comprehension(列表推导)

    前面好几次写过同一种循环:新建一个空列表,遍历另一组数据,逐个算出一个值 append 进去。这种写法太常见了,Python 为它提供了一种一行的写法。

  61. 第 61 页 · Four Lines Shorter

    同一件事的两种写法:左边用循环,右边用列表推导。

    右边这一行从左往右念:对 range(10) 里的每一个 x,取 x 的平方,放进新列表。先学会读,再学着写。

    列表推导不是必需的,任何列表推导都可以改写成循环。它的好处是把造新列表的过程写在一行里,读的人一眼就知道结果是一个列表。

  62. 第 62 页 · The Shape of a Comprehension

    列表推导的结构是固定的,方括号里三部分:表达式、for ... in ... 、可选的 if 条件。

    执行顺序和书写顺序不同:先执行 for,再判断 if,最后算表达式。所以读的时候先看中间那一段,就知道在遍历什么。

    前面那个三行三列的表格,用列表推导可以写成 [[0] * 3 for _ in range(3)]。

  63. 第 63 页 · Adding a Condition

    加上 if 条件,只有满足条件的项才会进入结果。

    注意 if 的位置。写在 for 后面是过滤,决定某一项要不要放进结果。写在最前面、配上 else,是第 4 讲的条件表达式,每一项都会放进结果,只是取值不同。这两种形式很容易弄混。

    过滤的写法里没有 else,因为不满足条件的就不要了,不需要第二个值。

  64. 第 64 页 · When Not to Use One

    列表推导不是用得越多越好。有三种情况不该用。

    第一,逻辑复杂,需要嵌套两三层 for 和好几个 if。这时写成普通循环更容易读懂。

    第二,每一轮要做的事不止「算一个值」,还要打印、写文件、修改别的变量。列表推导的用途是造一个新列表,不是执行一串动作。

    第三,结果根本用不到。只为了执行某些操作而写一个列表推导,会造出一个没人用的列表,白白占用内存。

    判断方法:写完念一遍,念不顺就改回循环。

  65. 第 65 页 · Deleting While Iterating

    最后一个常见错误:一边遍历一个列表,一边删除它的元素。

  66. 第 66 页 · A Loop That Skips Items

    这段代码想删掉所有的 2,结果还剩一个。

    for 循环在内部记着现在走到了第几项。删掉第一个 2 之后,后面的元素整体往前移了一位,第二个 2 移到了刚删掉的那个位置上;循环却照常走向下一个位置,于是第二个 2 被跳过了。

    这个错误有时候看起来是对的:要删的元素不相邻时,碰巧不会出问题;一旦相邻就会漏掉。时对时错的错误比每次都错的更难查。

  67. 第 67 页 · Correct Ways to Delete

    三种正确的写法,结果都是 [1, 3]。

    第一种用列表推导,把要保留的挑出来,造一个新列表。这种写法最清楚,一般首选。

    第二种遍历一份副本,在原列表上删除。b[:] 是副本,遍历副本的时候修改 b,不会影响循环。

    第三种反复删除,直到列表里没有 2 为止。写起来简单,但每次 in 和 remove 都要从头找,列表很长时会慢。

    记住这条规则:不要在遍历一个列表的同时改变它的长度。

  68. 第 68 页 · 随堂小测 · 第 2 轮

    第二轮小测,五道题。一题一题地放,每题答完当场公布。

    【第 1 题 · 下标和越界】

    在纸上把列表的每一项和它的下标写出来,正数下标写在上面,负数下标写在下面,然后按题目数一遍。凭感觉答很容易差一位。

    【第 2 题 · a = a.append(4) 之后打印什么】

    只有一个考点:原地修改的方法返回 None。append 已经把 4 加进去了,但它返回 None,None 又被赋回给 a,原来的列表就找不到了。

    【第 3 题 · 用 * 造矩阵】

    外层的乘法复制的是地址,三行是同一行。答错的同学,把那一页的两种写法各运行一遍,再对比每一行的 id。

    【第 4 题 · b = a 和 c = a[:] 的 id】

    b = a 复制的是地址,两个名字指向同一个列表,id 相同;c = a[:] 是切片,切片总是造一个新列表,id 不同。

    这一题和开场那道题问的是同一件事。

    【第 5 题 · 边遍历边删】

    答错的同学,把那段代码在自己电脑上运行一遍,在循环里加一行 print,把每一轮的 x 和当前的列表都打印出来,就能看清第二个 2 是怎么被跳过的。

  69. 第 69 页 · What to Take Away

    这一讲的要点都在这一页。

    列表存的是地址,不是数据;b = a 复制的是地址;切片和 copy 只复制最外层,嵌套结构要用 deepcopy;原地修改的方法返回 None;默认值不用列表;不要边遍历边删除。

    前两条是根本,其余都可以从它们推出来。如果只记一句话,就记住:赋值不复制对象。

  70. 第 70 页 · Lab 5 — 六道题,列表实战

    A 段六道题,覆盖列表的增删改查、切片、拷贝和嵌套结构。

    其中三道是有意安排的陷阱题,对应今天讲的三个常见错误:原地修改的方法的返回值、浅拷贝、边遍历边删。自己写错一次再找出原因,比听讲印象深得多。

    5-2 要求自己写 max,不许调用内置的 max。目的是练熟「遍历,同时记住当前最好的值」这个写法。找最小值、找第二大、找出现次数最多的,都是同一个结构。

    5-4 矩阵转置要用嵌套列表,造新矩阵时注意今天讲的「用乘号造矩阵」那个问题。

    A 段之外还有 B 段和 C 段,三段同时开放。C 段是命令行基本功,期末要考。

    提交到「小作业 2」,它覆盖第 4 到 6 周,10 月 27 日截止。

  71. 第 71 页 · Next Lecture

    下一讲讲元组、字典和集合,并补齐字符串的常用方法。

    列表几乎什么都能做,为什么 Python 还要另外三种容器?因为能做和适合做是两回事。下一讲会给出一个判断依据:遇到一个具体问题,该选哪一种。