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

列表 ​

一个变量只能存一个东西。而列表可以按顺序存很多东西,都放在一个名字下面。排行榜就是一串按名次排列的分数,一份测验就是一堆题目的集合。一旦你需要管理一组相关的值,你就需要列表了。

列表是 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():

不管 x 是什么,append(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),所以在循环里从头开始构建列表会不知不觉变成 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() 会抛出 ValueError。用 in 检查会多一次 O(n) 扫描,也就是要把列表遍历两次。对一次性代码来说这没什么问题。如果你更愿意捕获失败而不是提前检查,try / except ValueError 的用法会在文件与异常章节讲到。

先 in 再 remove() 这种模式要扫描两次 O(n),相当于把列表遍历了两遍。当顺序不重要时,把要删的元素和末尾元素交换、再 pop() 掉是 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() 有意返回 None;sorted() 接受任意可迭代对象并返回一个新列表。

常用操作 ​

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 会从一开始编号),并不会改变循环在列表里实际读取的位置。比起手写计数器更值得用它,原因不是速度,而是一个自己手动累加的计数器多了一件容易忘记递增的事,而 enumerate() 不会出现这种脱节。

python
players = ["小明", "小红", "小刚"]

for player in players:
    print(player)

for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")
# 1. 小明
# 2. 小红
# 3. 小刚

for 和 enumerate() 会在控制流章节详细讲解。简单来说: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]。修改内层列表会影响外层列表,因为外层列表持有的是指向同一个对象的引用。

嵌套列表并不是真正的二维数组。外层列表存的是指向内层列表的引用,每个内层列表都是独立的对象,所以每一行的长度和类型都可以不一样。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嵌套列表 它不是真正的二维数组:外层列表存的是引用,所以内层每一行的长度和类型都可以不同,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"前三名: {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]]  (没有变化)