Skip to content
This page has been auto-translated and may contain errors.View in English

列表

docs.scrimba.com

一个变量存放一件东西。一个列表按顺序存放许多东西,都放在一个名字下面。排行榜是一个排名的分数序列。一个测验是一组问题的集合。一旦你需要管理一组相关的值,你就需要一个列表。

列表是 Python 的通用有序可变序列。它们是任何随时间变化的数据的自然选择:添加或删除项、重新排列顺序、过滤或排序内容。当顺序重要且集合会改变时,列表通常是最好的首选。

list 是 Python 的动态数组:一个有序的、可变的(可以就地更改)序列。性能特征驱动着设计决策。按索引读取是 O(1),常数时间,无论列表有多大(大 O 是成本如何随大小 n 变化的速记)。append() 也基本上是 O(1):列表在末尾保留备用空间,只偶尔重新调整大小。成本高的操作是 insert()remove(),都是 O(n),因为变化点之后的每个元素都会移动。当你发现自己在循环中在前面插入或删除时,这个 O(n) 是信号,应该考虑不同的数据结构。

创建列表

方括号,值用逗号分隔。列表可以包含任何类型的混合,空列表作为起点是有效且常见的,你可以随着时间的推移逐步构建。

列表用方括号语法定义,并保留插入顺序。它们可以包含任何 Python 值,包括其他列表。空列表 [] 是当你逐步积累项时的标准起点。

方括号字面量创建一个新的 list 对象,列表存储对其元素的引用,而不是复制的值(引用是指向真实对象所在位置的指针)。所以元素可以是任何 Python 对象,包括另一个列表。在一个列表中混合类型是有效的,但在快速脚本之外很少见:大多数你保留的列表只包含一种类型,这使得它们更容易推理和在循环中处理。

python
scores = [87, 92, 74, 65, 91]
players = ["李明", "王强", "张芳"]
mixed = ["李明", 87, True, 3.14]   # 任何类型,尽管不常见
empty = []
Juno创建列表 方括号之间的值,用逗号分隔,Python 保持你写的顺序。一个空 [] 是一个很好的起点,当你计划逐渐填充它时。我的大多数列表都是这样开始的,空的,等待被填充。
Juno创建列表 方括号定义一个列表,插入顺序被保留。一个列表可以包含任何值,包括其他列表,`[]` 是当你逐步积累项时的标准起点。
Juno创建列表 一个列表保存对其元素的引用,所以它可以混合任何对象,尽管每个列表一种类型是你真正想要保留的。采用 `[]` 并逐步构建,过度分配使这很便宜。

索引和切片

列表使用与字符串相同的编号:位置从 0 开始,负数从末尾向后计数。你可以通过位置读取任何项。因为列表是可变的,你也可以写入到特定位置。

列表索引和切片遵循与字符串相同的规则。关键区别是可变性:你可以分配给索引或切片以就地更改项,字符串不允许这样做。

索引和切片的读取与字符串相同,但列表让你也可以赋值,因为它是可变的。要知道的一个是切片赋值:它可以改变列表的长度。lst[1:3] = [10, 20, 30] 用三个新项替换位置 1 和 2 的两个项,增加列表的长度一。替换不必与切片的长度匹配,这很方便,是一个常见的惊喜,所以要刻意使用它,而不是意外地使用。

python
scores = [87, 92, 74, 65, 91]

scores[0]      # 87  (第一个)
scores[-1]     # 91  (最后一个)
scores[1:3]    # [92, 74]
scores[:2]     # [87, 92]
scores[::-1]   # [91, 65, 74, 92, 87]  (反向)

scores[0] = 90   # 可变:有效(字符串会抛出 TypeError)
Juno索引和切片 位置从 0 开始,负数从末尾向后计数,所以 scores[-1] 是最后一个。一个像 scores[1:3] 这样的切片给你一个包含位置 1 和 2 的新列表。让我花了一会儿的部分:对于列表,你也可以写入一个位置,scores[0] = 90,而字符串会拒绝。
Juno索引和切片 与字符串相同的索引和切片,位置从 0 开始,负数从末尾。区别是可变性:分配给索引或切片,你就改变了列表,而字符串会抛出 TypeError
Juno索引和切片 读起来像字符串索引,但你可以赋值,切片赋值可以改变列表大小。lst[1:3] = [10, 20, 30] 将两个项交换为三个,有目的时很方便,意外时是个惊喜,所以要故意使用它。

添加项

有三种方法可以添加项。append() 在末尾添加单个项,这几乎是你每次都会使用的。insert() 在特定位置添加。extend() 合并另一个列表。

append() 是 O(1) 摊销的(列表保留备用空间,只偶尔重新调整大小,所以平均成本保持常数),是逐项构建列表的标准方式。insert() 是 O(n),因为它移动后续元素。extend() 等同于 +=,比循环中重复 append() 更高效。

append() 很便宜,因为列表保留备用空间,只偶尔重新调整大小,所以平均成本保持 O(1)(常数,无论长度多少)。insert(0, x) 是陷阱:在前面插入是 O(n),因为每个元素向右移动一个位置。如果你通过反复添加到前面来构建列表,那会变成整个过程的 O(n 平方)。当你需要快速前面插入时,使用标准库中的 collections.deque,其 appendleft 是 O(1),在模块章节中覆盖。extend() 一次性增长列表,这就是为什么当你已经有项时,它比 append() 调用的循环更快。

python
scores = [87, 92, 74]

scores.append(65)          # [87, 92, 74, 65]
scores.insert(1, 100)      # [87, 100, 92, 74, 65]
scores.extend([55, 71])    # [87, 100, 92, 74, 65, 55, 71]

常见的错误:用一个列表调用 append() 会添加整个列表作为一个项,给你一个列表内的列表。使用 extend() 来合并:

append(x) 总是添加 x 作为一个单独的元素。将一个列表传递给 append() 会给你一个嵌套的列表。当你想要合并来自另一个列表的所有项到这个列表中时,使用 extend()

append(x) 添加 x 作为一个元素,无论 x 是什么,所以给它一个列表会整体嵌套那个列表。extend(iterable) 遍历参数并分别添加每个元素。lst += other 做与 extend 相同的事情,所以使用哪个更清晰就用哪个。

python
scores.append([55, 71])    # [..., [55, 71]]  嵌套列表,可能有问题
scores.extend([55, 71])    # [..., 55, 71]    合并,正确
Juno添加项append() 将一个项添加到末尾,这几乎每次都是你会使用的。insert() 在位置放入一个项,extend() 合并另一个列表。经典的错误:append() 一个列表,你会得到列表中的列表,所以当你想合并时使用 extend()
Juno添加项append() 是你的默认,逐项添加到末尾。insert() 移动其后的所有东西,所以成本更高。append() 一个列表,它嵌套;extend()(或 +=)合并项。
Juno添加项append() 是摊销 O(1),insert(0, x) 是 O(n),所以在循环中从前面构建会静默变成二次方,在那里使用 deque。而 append() 一个列表会整体嵌套它;extend()+= 是合并。

移除项

有四种工具来移除项。remove() 按值搜索。pop() 按位置移除并将项返回给你。del 按位置移除,没有返回值。clear() 清空整个列表。

remove() 是 O(n):它按值扫描第一个匹配。pop() 没有参数是 O(1) 移除最后一项。pop(i) 对任何其他位置是 O(n),因为元素移动。del scores[i] 等同于 pop(i) 但丢弃返回值。

remove(value) 从前面扫描比较每个元素直到找到匹配,然后将其后的所有东西向左移动:O(n)(成本随列表长度增长)。pop() 从末尾是 O(1),没有东西移动。pop(i) 在其他地方是 O(n),原因相同。如果你发现自己在热循环中从任意位置移除,那重复的 O(n) 是信号,应该重构数据或选择不同的集合。

python
scores = [87, 92, 74, 65, 91]

scores.remove(74)    # 移除第一个 74
scores.pop()         # 移除并返回最后一项 (91)
scores.pop(0)        # 移除并返回位置 0 的项 (87)
del scores[1]        # 移除位置 1,没有返回值
scores.clear()       # 移除所有东西

如果列表中没有该值,remove() 会抛出 ValueError。如果你不确定,先用 in 检查:

python
if 74 in scores:
    scores.remove(74)

remove() 在未找到时抛出 ValueErrorin 检查添加了额外的 O(n) 扫描,所以列表被遍历两次。对于一次性代码这很好。当你宁愿捕获失败也不想预先检查时,try / except ValueError文件和异常章节中覆盖。

in 然后 remove() 模式是两个 O(n) 扫描,列表被遍历两次。当顺序无关紧要时,将目标与最后一个元素交换并弹出是 O(1)(常数成本,无移动)。而如果你经常测试成员资格,一个 set 在 O(1) 中查找,而列表是 O(n),在元组和集合章节中覆盖。经验法则:一个列表用于有序的、变化的数据,一个集合在"它在里面吗"是热问题时。

Juno移除项remove() 按值删除,只有第一个匹配,如果值不在那里会抛出 ValueError,所以当你不确定时先用 in 检查。pop() 按位置移除并返回项,del 按位置移除并什么都不返回给你。
Juno移除项remove() 删除第一个值匹配,在未找到时抛出 ValueError。从末尾 pop() 很便宜,返回项;pop(i) 在其他地方移动其后的所有东西。del 按位置移除,无返回。
Juno移除项 远端的 remove()pop(i) 是 O(n) 移动;只有从尾部 pop() 是 O(1)。如果"它在里面吗"是热路径,一个 set 在 O(1) 中做,而列表是 O(n)。在 remove() 前预先检查 `in` 会遍历列表两次。

排序

sorted() 返回一个全新的已排序列表,留下你的原始列表未触及。.sort() 就地排序列表并返回 None。这个区别的重要性超出了人们的想象。

sorted() 是安全的默认:它从不修改原始。.sort() 就地修改并返回 None,这是常见的陷阱。分配 .sort() 的结果给你 None,不是排序后的列表。当你需要原始保持不变时使用 sorted();当你只想要排序的版本时使用 .sort()

Python 的排序是稳定的:相等的元素保持它们原始的相对顺序。这就是让你按一个键排序,然后按另一个再排序,第一个顺序作为平局分解器存活的原因。它在几乎排序的数据上也很快,接近 O(n)(线性)而不是从零开始排序的 O(n log n),所以重新排序一个几乎有序的列表很便宜。.sort() 有意返回 None:改变列表就地的方法不也返回它,这是防止你写 x = lst.sort() 并悄悄得到 None 的约定。sorted() 接受任何可迭代的,不只是列表,总是返回一个新列表。

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)            # [42, 55, 71, 87, 96] (新列表)
scores.sort()                      # 就地排序,返回 None
scores.sort(reverse=True)          # [96, 87, 71, 55, 42]

result = scores.sort()             # result 是 None,不是排序后的列表
Juno排序sorted() 给你一个新的排序列表,留下原始不变。.sort() 重新排列列表本身并返回 None,所以 x = scores.sort() 会让你得到 None,不是列表。我在它真正记住之前做过多次这个确切的错误。
Juno排序sorted() 是安全的默认,它从不触及原始。.sort() 就地排序并返回 None,所以分配其结果是要记住的陷阱。当你需要原始保持不变时选择 sorted()
Juno排序 排序是稳定的,所以先按次要键排序,然后按主键,平局保持它们的先前顺序。它在几乎排序的数据上运行接近线性,所以重新排序很便宜。.sort() 有意返回 Nonesorted() 接受任何可迭代的并返回新列表。

有用的操作

Python 有一组内置工具直接对列表工作。len()sum()min()max() 是你将不断使用的四个。

内置序列函数对任何列表工作。in 是列表的线性扫描;如果你需要快速的重复成员资格测试,转换为集合。.index() 如果值不在列表中会抛出 ValueError

len()sum()min()max() 对任何可迭代的工作,不只是列表,所以相同的调用读取列表、元组或生成器。重要的成本注意:in.index() 都从前到后扫描,O(n)(成本随长度增长),因为列表没有其内容的索引。循环中重复的成员资格检查是成本增加的地方,一个 set 是修复。一个尖锐的边缘:sum()0 开始,所以它添加数字,不是字符串;要连接字符串使用 "".join(),在字符串章节中覆盖。

python
scores = [87, 92, 74, 65, 91]

len(scores)          # 5
sum(scores)          # 409
min(scores)          # 65
max(scores)          # 92
scores.count(87)     # 1
scores.index(74)     # 2
74 in scores         # True
74 not in scores     # False
scores.copy()        # 浅复制
scores.reverse()     # 就地反向
Juno有用的操作len()sum()min()max() 都直接对列表工作,无需设置。in 询问某物是否存在,.count() 统计出现多少次,.index() 找到第一个匹配的位置。
Juno有用的操作 内置函数对任何列表工作,序列函数对任何可迭代的工作。in 是线性扫描,所以对于重复的成员资格测试转换为集合。.index() 在值未被找到时抛出 ValueError
Juno有用的操作len()sum()min()max() 接受任何可迭代的,不只是列表。in.index() 都是 O(n) 扫描,所以热成员资格检查想要 set。而 sum()0 开始,所以对于字符串使用 "".join()

迭代

一个 for 循环逐个遍历列表的项。for 后的变量依次接收每个项。当你也需要位置时,enumerate() 在不使用手动计数器的情况下给你两者。

for item in list 调用列表的迭代器并在每一步推进它。enumerate(iterable, start=0) 包装迭代器并产生 (index, value) 对。使用 enumerate() 比维护计数器变量更清晰且不易出错。

enumerate() 在你循环时产生 (index, value) 对,这就是为什么 for i, item in enumerate(...) 读起来干净:每对解包到两个名字。start 参数只移动计数器进行显示(所以 start=1 从 1 开始编号),不改变循环实际读取列表的位置。比手卷计数器更好的原因不是速度,是一个你手动增加的单独计数器是你忘记增加的另一件东西,而 enumerate() 不能漂移不同步。

python
players = ["李明", "王强", "张芳"]

for player in players:
    print(player)

for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")
# 1. 李明
# 2. 王强
# 3. 张芳

forenumerate()控制流章节获得完整处理。简短版本:for player in players 为每个项运行一次,enumerate() 在每次迭代时给你位置和值。

Juno迭代 一个 `for` 循环逐个遍历列表,`for` 后的名字在每一轮变成那个项。当你也想要位置时,`enumerate()` 给你两者,所以你永远不必自己保持一个计数器。让它为我计数为我节省了一堆非一错误。
Juno迭代 `for item in list` 依次访问每个项。`enumerate(iterable, start=0)` 产生 `(index, value)` 对,比你手动管理的计数器更清晰且不易出错。当你想要基于 1 的编号时使用 `start=1`。
Juno迭代 `enumerate()` 产生 `(index, value)` 对,直接解包到两个名字,`start` 只移动显示的计数器,不移动读取位置。获胜不是速度,是一个手动计数器可以漂移不同步,这个不能。

嵌套列表

一个列表可以包含其他列表。这是你如何表示网格或表格的方式:行的列表,每行是值的列表。两组方括号访问一个项:第一个选择行,第二个选择列。

嵌套列表是列表引用的列表。每个内部列表是一个独立的对象。用链式下标访问:grid[row][col]。改变内部列表影响外部列表,因为外部列表保存对同一对象的引用。

嵌套列表不是真正的 2D 数组。外部列表保存对内部列表的引用,每个都是自己独立的对象,所以行可以有不同长度并保存不同类型。grid[1][2] 是两个查找:选择行,然后索引到其中。抓住人们的后果:外部列表的浅复制(.copy() 产生的那种)复制外部容器但仍然指向相同的内部列表,所以对内部行的更改在两个副本中显示。下面的可变性部分使这具体化。

python
grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

grid[0]       # [1, 2, 3]
grid[1][2]    # 6  (行 1,列 2)
Juno嵌套列表 一个列表可以保存其他列表,这是你如何构建网格或表格的方式:行的列表,每行一个列表。两组括号伸进去,grid[1][2],第一个选择行,第二个从中选择。
Juno嵌套列表 嵌套列表是对其他列表的引用的列表,每个内部列表其自己的对象。伸进去用链式下标,grid[row][col]。因为外部列表只保存引用,改变内部列表通过指向它的每个名字显示。
Juno嵌套列表 不是真正的 2D 数组:外部列表保存引用,所以内部行可以变化长度和类型,grid[1][2] 是两个查找。浅复制复制外部列表但共享内部,这是下一部分落地的陷阱。

可变性:陷阱

这令几乎每个人都惊讶。将列表分配给新变量不会制作复制。两个名字指向同一个列表。改变一个,你改变另一个。要获得独立的副本,你必须明确要求一个。

列表赋值复制引用,不是对象。两个名字指向同一个底层列表。通过任何一个名字的改变影响同一数据。当你需要独立的数据时,用 .copy()list() 或完整切片 [:] 显式复制。

b = a 指向同一列表对象的第二个名字,所以通过 b 的任何改变也落地到 a 看到的对象。.copy()a[:] 制作浅复制:一个新外部列表,保存与原始相同的元素引用。对于不可变值的平面列表(数字、字符串、不能就地改变的东西),那就是你需要的全部。对于嵌套列表,它是一个陷阱,因为内部列表仍然在原始和副本之间共享,所以编辑一行通过一个在另一个中显示。

python
a = [1, 2, 3]
b = a            # b 不是副本;它指向同一个列表

b.append(4)
print(a)         # [1, 2, 3, 4]  (改变:a 和 b 是同一个列表)
python
b = a.copy()    # 独立副本
b = list(a)     # 相同结果
b = a[:]        # 也是相同

# 嵌套列表仍然共享它们的内部对象:
matrix = [[1, 2], [3, 4]]
copy = matrix.copy()

copy[0].append(99)
print(matrix)   # [[1, 2, 99], [3, 4]]  (内部列表被共享)

对于需要完全独立的嵌套结构,手动复制每个内部列表,或从标准库使用 copy.deepcopy(),在模块章节中覆盖。

Juno可变性:陷阱b = a 不制作副本,两个名字指向同一个列表,所以通过一个的改变通过另一个显示。对于独立列表,你必须要求:.copy()list(a)a[:]。这个第一次令几乎每个人都惊讶,包括我。
Juno可变性:陷阱 赋值复制引用,不是列表,所以 b = a 在一个对象上留下两个名字,通过其中任何一个的改变命中同一数据。当你需要独立时用 .copy()list(a)a[:] 显式复制。
Juno可变性:陷阱b = a 共享一个对象;.copy()a[:] 制作浅复制,一个新外部列表在相同内部引用。不可变值的平面列表是安全的,嵌套的共享它们的行,所以当你也需要内部独立时用 copy.deepcopy()

更多方法

方法做什么
.append(item)添加到末尾
.insert(i, item)在位置 i 插入
.extend(iterable)从可迭代的添加所有项
.remove(value)移除值的第一个匹配
.pop(i)移除并返回位置 i 的项(默认:最后一个)
.clear()移除所有项
.index(value)第一个匹配的位置
.count(value)匹配的数量
.sort()就地排序
.reverse()就地反向
.copy()返回浅复制

实践中

构建一个分数跟踪器:添加结果,排序它们,并打印摘要。

python
scores = []

scores.append(87)
scores.append(54)
scores.append(92)
scores.append(67)
scores.append(45)

scores.sort(reverse=True)

print(f"排序的分数:{scores}")
print(f"最高:{scores[0]}")
print(f"最低:{scores[-1]}")
print(f"平均值:{sum(scores) / len(scores):.1f}")
print(f"前 3:{scores[:3]}")

两个名字和分数的平行列表:找到最高的表现者并打印排序结果。

python
names = ["李明", "王强", "张芳", "大卫"]
scores = [87, 74, 92, 55]

best_score = max(scores)
best_index = scores.index(best_score)
best_player = names[best_index]

print(f"最高的播放器:{best_player} ({best_score})")
print(f"平均值:{sum(scores) / len(scores):.1f}")

ranked = sorted(scores, reverse=True)
print(f"分布(排序):{ranked}")

for rank, score in enumerate(ranked, start=1):
    print(f"  排名 {rank}{score}")

演示别名和复制之间的区别,以及嵌套列表的浅复制和深复制之间的区别。

python
# 别名:b 不是副本
a = [1, 2, 3]
b = a
b.append(4)
print(a)    # [1, 2, 3, 4]  (同一对象)

# 浅复制:外部列表是独立的,内部列表被共享
matrix = [[1, 2, 3], [4, 5, 6]]
shallow = matrix.copy()
shallow[0].append(99)
print(matrix)    # [[1, 2, 3, 99], [4, 5, 6]]  (内部列表被共享)

# 用 for 循环手动深复制(无需导入)
matrix = [[1, 2, 3], [4, 5, 6]]
deep_copy = []
for row in matrix:
    deep_copy.append(row[:])    # 显式复制每个内部列表

deep_copy[0].append(99)
print(matrix)    # [[1, 2, 3], [4, 5, 6]]  (未改变)