Day13 · 列表与元组
阶段:阶段一 · 核心基础 | 前置:Day12 | 技术基准:Python 3.15.0
学习目标
- 掌握列表(list)的定义、索引、切片、遍历与常用方法
- 理解列表是"可变、有序、元素可异构"的序列
- 掌握列表生成式,能写出简洁高效的列表构造代码
- 掌握元组(tuple)的定义、单元素陷阱、不可变性与拆包
- 会做列表/元组与其他类型之间的显式类型转换
从今天开始进入 Python 最常用的"容器"类型。前面我们用变量只能存一个数,而真实业务要存"全班 50 个同学的成绩""购物车几十件商品"------这就需要能装一堆数据的容器。本章先讲两个有序序列:列表(可变)和元组(不可变)。
一、列表 list 是什么
列表是对传统数组的高级封装,但比数组更强大:大小可变、元素类型可以不同、有序、可通过索引访问 。定义用方括号 [],元素之间用逗号 , 隔开。
特点一览:
- 元素按顺序排列,通过
下标访问(下标从 0 开始); - 元素类型可以各不相同(数字、字符串、甚至另一个列表都行);
- 大小可以动态增删,没有固定上限;
- 是可变(mutable)类型,可以原地修改某个元素。
python
# Author: XiaoJia
# 字面量创建
empty = [] # 空列表
nums = [1, 2, 3, 4, 5]
mix = [1, "小嘉", 3.14, True] # 元素类型可以不同
# 构造函数创建
print(list()) # []
print(list("Python")) # ['P','y','t','h','o','n'] 字符串拆成字符列表
print(list(range(1, 6))) # [1, 2, 3, 4, 5]
二、索引与切片
2.1、索引访问
python
# Author: XiaoJia
data = [10, 20, 30, 40, 50]
print(data[0]) # 10 第一个元素,下标从 0 开始
print(data[2]) # 30
print(data[-1]) # 50 负数下标从右往左数
print(data[-2]) # 40
data[1] = 200 # 列表可变,可以通过下标改元素
print(data) # [10, 200, 30, 40, 50]
下标越界会报 IndexError:data[5](最大下标是 4)直接崩。
2.2、切片
切片语法 列表[开始:结束:步长],含头不含尾:
python
# Author: XiaoJia
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(data[2:5]) # [2, 3, 4]
print(data[:3]) # [0, 1, 2] 从头切到 3
print(data[7:]) # [7, 8, 9] 从 7 切到尾
print(data[::2]) # [0, 2, 4, 6, 8] 步长 2
print(data[::-1]) # [9,8,7,...,0] 步长 -1 逆序
print(data[2:100]) # [2,3,...,9] 结束下标越界也不报错
切片还能用来批量替换(Day15 会再深入):
python
# Author: XiaoJia
data = [0, 1, 2, 3, 4]
data[0:2] = [100, 200]
print(data) # [100, 200, 2, 3, 4]
三、遍历列表
python
# Author: XiaoJia
data = [10, 20, 30]
# 最常用:for...in 直接拿元素
for item in data:
print(item, end=" ")
print() # 10 20 30
# 同时拿下标:enumerate
for i, v in enumerate(data):
print(i, v)
# 求和
print("总和", sum(data)) # 60
四、列表常用方法
| 方法 | 作用 |
|---|---|
append(x) |
末尾追加一个元素 |
extend(可迭代) |
末尾批量追加多个元素 |
insert(i, x) |
在下标 i 处插入 x |
remove(x) |
删除第一个值为 x 的元素 |
pop() / pop(i) |
弹出末尾 / 下标 i 的元素 |
index(x) |
找 x 第一次出现的下标 |
count(x) |
统计 x 出现几次 |
sort() |
原地升序排序 |
reverse() |
原地反转 |
clear() |
清空 |
copy() |
浅拷贝一份 |
python
# Author: XiaoJia
data = [3, 1, 4, 1, 5]
data.append(9) # 末尾加一个
data.insert(0, 0) # 最前面插一个
print(data) # [0, 3, 1, 4, 1, 5, 9]
data.remove(1) # 删除第一个 1
print(data) # [0, 3, 4, 1, 5, 9]
print(data.pop()) # 9 弹出末尾
print(data.pop(0)) # 0 弹出下标 0
print(data.count(1)) # 1
print(data.index(4)) # 1
data.sort() # 原地升序
print(data) # [1, 3, 4, 5]
data.reverse()
print(data) # [5, 4, 3, 1]
data.extend([7, 8]) # 批量追加
print(data) # [5, 4, 3, 1, 7, 8]
append 和 extend 的区别是新手高频考点:
python
# Author: XiaoJia
a = [1, 2]
a.append([3, 4]) # 把整个列表当一个元素塞进去
print(a) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4]) # 把列表里的元素逐个加进去
print(b) # [1, 2, 3, 4]
sorted() vs list.sort():sort() 原地改、返回 None;sorted() 返回一个新列表、不动原列表:
python
# Author: XiaoJia
nums = [3, 1, 2]
new = sorted(nums) # 返回新列表
print(nums) # [3, 1, 2] 原列表没变
print(new) # [1, 2, 3]
五、列表生成式
用一行表达式从旧序列快速构造新列表,是 Python 最著名的特色写法之一。语法只有一句话:
python
[表达式 for 变量 in 序列 if 条件]
5.1、基础语法:从旧序列构造新列表
python
# Author: XiaoJia
# 传统写法:先建空列表,再循环逐个 append
result = []
for i in range(1, 6):
result.append(i * i)
print(result) # [1, 4, 9, 16, 25]
# 列表生成式一行搞定
print([i * i for i in range(1, 6)]) # [1, 4, 9, 16, 25]
读法:[表达式 for 变量 in 序列] 就是"对序列里每个元素,套进表达式,收集成一个新列表",表达式中用到遍历变量 i。它不只对 range 用,对任何可迭代对象(列表、字符串、元组......)都可以:
python
# Author: XiaoJia
words = ["python", "java", "go", "c"]
print([w.upper() for w in words]) # ['PYTHON','JAVA','GO','C']
# 对字符串(字符序列)做变换
print([ch + "!" for ch in "abc"]) # ['a!','b!','c!']
# 对已有列表做数学变换
nums = [1, 2, 3, 4]
print([n * 10 + 1 for n in nums]) # [11, 21, 31, 41]
# 从字符串拆出每个字符再处理
print([int(d) for d in "1234"]) # [1, 2, 3, 4]
5.2、带 if 过滤
在 for 后面加 if 条件,只有满足条件的元素才会进入结果:
python
# Author: XiaoJia
# 1~10 里的奇数
print([i for i in range(1, 11) if i % 2 == 1]) # [1,3,5,7,9]
# 从单词里只挑长度 >= 3 的
words = ["我", "Python", "AI", "数据", "TensorFlow"]
print([w for w in words if len(w) >= 3]) # ['Python','数据','TensorFlow']
# 过滤负数
print([n for n in [-3, 1, -2, 5, 0] if n > 0]) # [1, 5]
顺序:for 在前、if 在后,if 起"筛选"作用。想对不符合条件的元素做"替换"而不是丢弃,用下一节的 if-else。
5.3、if-else 三元变换(过滤后再改值)
当你想"保留所有元素、但对不同元素给不同值"时,把 if-else 写回表达式里 (位置在 for 之前):
python
# Author: XiaoJia
# 正数保留,负数归零
print([x if x > 0 else 0 for x in [-3, 1, -2, 5]]) # [1, 0, 0, 5]
# 偶数是原值,奇数变成它的负值
print([x if x % 2 == 0 else -x for x in range(1, 6)]) # [-1, 2, -3, 4, -5]
两种 if 别搞混:
[x for x in 序列 if 条件]------if在后面,作用是"筛掉不满足的",结果元素个数会变少;[x if 条件 else y for x in 序列]------if-else在前面,作用是"逐个选值",结果元素个数不变。
5.4、嵌套循环:一行写出多重 for
生成式里可以写多个 for,等价于嵌套循环:
python
# Author: XiaoJia
# 字母 × 数字
print([x + y for x in "AB" for y in "12"]) # ['A1','A2','B1','B2']
# 二维矩阵摊平成一维
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6]
把它拆开看就懂了,等价写法:
python
# Author: XiaoJia
matrix = [[1, 2], [3, 4], [5, 6]]
flat = []
for row in matrix: # 第一个 for
for num in row: # 第二个 for
flat.append(num) # 收集
print(flat) # [1, 2, 3, 4, 5, 6]
读诀窍:从左往右读 for,最左边的先循环,最后面跟的是要收集的表达式 。[num for row in matrix for num in row] = 先取 row,再从 row 里取 num,收集 num。
5.5、可读性:什么时候用、什么时候别硬用
列表生成式追求"一行简洁",但不是越短越好:
python
# Author: XiaoJia
# 推荐:一目了然
squares = [i * i for i in range(10)]
# 勉强可以:两个 for + 一个 if,已接近可读性极限
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
even = [n for row in matrix for n in row if n % 2 == 0]
# 别硬写:超过三层嵌套、或逻辑复杂时,回到普通 for 循环更清晰
# 用循环不是"退步",而是"可读性优先"(呼应 Day01 的 Python 之禅)
经验法则:单个 for + 最多一个 if,随便写;两个 for,看清层级再写;三个及以上 for,果断改用普通循环。
5.6、性能:生成式通常比循环更快
生成式在 CPython 里是高度优化的,通常比"手动建空列表 + append"更快,代码也更短:
python
# Author: XiaoJia
# 时间对比(timeit 统计 1 万次构造 1~1000 平方列表的耗时)
import timeit
def by_loop():
result = []
for i in range(1, 1001):
result.append(i * i)
return result
def by_comprehension():
return [i * i for i in range(1, 1001)]
t1 = timeit.timeit(by_loop, number=10000)
t2 = timeit.timeit(by_comprehension, number=10000)
print(f"循环耗时: {t1:.3f}s") # 本机约 0.29s
print(f"生成式耗时: {t2:.3f}s") # 本机约 0.26s,略快(数值随机器波动)
结论:生成式更简洁、通常也更快,是构造列表的首选。真正的性能差异在数据量大时才明显,日常规模两者都能跑。它的更多进阶形态(字典/集合推导式、生成器表达式、性能再对比)在 Day15 展开。
六、元组 tuple 是什么
元组和列表几乎一样,区别只有一个:元组不可变(immutable) ------创建之后不能增、删、改元素。定义用圆括号 ()。
python
# Author: XiaoJia
t = (1, 2, 3)
print(t[0]) # 1
print(len(t)) # 3
# t[0] = 99 # TypeError:元组不支持修改
6.1、单元素元组的陷阱
python
# Author: XiaoJia
a = (123) # 这不是元组!就是整数 123,括号被当成普通括号
print(type(a)) # <class 'int'>
b = (123,) # 必须加逗号,才是单元素元组
print(type(b)) # <class 'tuple'>
一个元素的元组后面必须跟逗号 ,这是规定,别问为什么。空元组则直接 ()。
6.2、元组的运算与方法
python
# Author: XiaoJia
t1 = (1, 2)
print(t1 + (3, 4)) # (1, 2, 3, 4) 拼接,产生新元组
print(t1 * 3) # (1, 2, 1, 2, 1, 2) 重复
t = (1, 2, 3, 2, 1)
print(t.count(2)) # 2
print(t.index(3)) # 2
print(max(t), min(t)) # 3 1
因为不可变,元组没有 append/sort/remove 这些修改自身的方法,只有 count 和 index。
6.3、拆包(unpacking)
把一个元组(或列表)里的元素一次性赋给多个变量:
python
# Author: XiaoJia
point = (3, 4)
x, y = point
print(x, y) # 3 4
# 经典应用:交换两个变量的值(不需要中间临时变量)
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# 带星号的拆包
first, *rest = [1, 2, 3, 4]
print(first) # 1
print(rest) # [2, 3, 4]
6.4、namedtuple 简介
元组的元素只能靠下标 t[0]、t[1] 访问,可读性差。collections.namedtuple 让元组元素可以"按名字"访问:
python
# Author: XiaoJia
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4 下标照样能用
七、列表 vs 元组对比
| 对比项 | 列表 list | 元组 tuple |
|---|---|---|
| 定义符号 | [] |
() |
| 可变性 | 可变,能增删改 | 不可变 |
| 方法数量 | 多(append/sort 等) | 少(只有 count/index) |
| 性能 | 稍慢 | 稍快、更省内存 |
| 能否做字典 key | 不能(可变不可哈希) | 能 |
| 典型用途 | 同类型数据的动态集合 | 固定记录、多返回值、拆包 |
经验法则:数据需要变就用列表,数据定下来不想被改就用元组。函数一次返回多个值,Python 底层就是打包成元组返回的。
八、显式类型转换
Day06 提过,这里结合容器系统列一下常用内建转换函数:
python
# Author: XiaoJia
print(int("18")) # 18 字符串 → 整数
print(float("3.14")) # 3.14
print(str(100)) # "100" 数字 → 字符串
print(list("abc")) # ['a','b','c'] 字符串 → 列表
print(tuple([1, 2])) # (1, 2) 列表 → 元组
print(list((1, 2))) # [1, 2] 元组 → 列表
print(set([1, 1, 2])) # {1, 2} 列表 → 集合(自动去重,Day14 讲)
# 常用统计函数,对列表/元组都适用
nums = [3, 1, 4, 1, 5]
print(len(nums), max(nums), min(nums), sum(nums)) # 5 5 1 14
九、栈、队列与元组的"不可变假象"
9.1、用列表当栈和队列
append() 在末尾加、pop() 从末尾弹,正好构成"后进先出"的栈(stack):
python
# Author: XiaoJia
stack = []
stack.append("第一")
stack.append("第二")
print(stack.pop()) # 第二 后进先出
print(stack.pop()) # 第一
但用列表当**队列(queue,先进先出)**就很不合适:pop(0) 要把后面所有元素整体前移,数据一大非常慢。真要高效队列,标准库 collections.deque 才是正解(两端操作都是 O(1)),这里先建立概念,Day27 会用。
9.2、赋值是引用,不是拷贝
b = a 只是给同一个列表再起个名字,不是复制:
python
# Author: XiaoJia
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] 改 b 连 a 一起变
想要一份互不影响的副本,用 a.copy()、a[:] 或 list(a)。注意这只是"浅拷贝",列表里若还套着列表,内层仍共享------完整解释见 Day15。
9.3、元组不可变的"假象"
元组本身不可变,但它里面装的那个列表可是可变的:
python
# Author: XiaoJia
t = (1, [2, 3])
# t[0] = 99 # TypeError:元组元素不能重新赋值
t[1].append(4) # 合法!改的是那个列表内部,不是元组的指向
print(t) # (1, [2, 3, 4])
这并不矛盾:元组保证的是"它指向的东西不变",至于它指向的列表自己怎么变,元组管不着。所以元组可哈希的前提是:里面装的全是不可变对象。反过来用一个"里面套了列表"的元组去做字典 key,照样会报 TypeError: unhashable type: 'list',原因就在这里------不是元组不哈希,是它内部那个列表不哈希。
练习题
- 定义列表
[7, 3, 9, 2, 5],用切片取出中间三个元素,再用sort()升序排列并打印。 - 用列表生成式生成 1~10 的平方数列表,再用
filter思路(生成式加 if)只保留偶数平方。 - 写一个元组
(10, 20, 30),用拆包把它赋给三个变量,然后在不引入第三个变量的情况下交换前两个。 - 解释:为什么
(100)不是元组?怎么写才是单元素元组? - 给定列表
["张三", "李四", "王五"],用enumerate输出1. 张三这样的编号列表。 - 思考题:什么场景下你会优先用元组而不是列表?为什么元组能当字典的 key 而列表不行?
- 用列表生成式,从一个句子
s = "I love Python and data"里提取所有长度不小于 4 的单词(提示:s.split()先切成单词列表)。 - 用列表生成式把摄氏温度列表
[0, 20, 37, 100]转换为华氏(公式F = C * 9 / 5 + 32),结果保留一位小数(可用round或 f-string)。 - 用嵌套生成式把二维列表
[[1, 2], [3, 4], [5, 6]]摊平,再只保留其中的奇数。
小结
- 列表
[]:有序、可变、元素可异构;支持索引、切片、增删改查。 - 常用方法:append/extend/insert/remove/pop/index/count/sort/reverse;注意 append 是塞一个,extend 是拆进去。
- 列表生成式
[表达式 for x in 序列 if 条件]是 Pythonic 的列表构造方式。 - 两种
if区分:for后的if是"筛掉",表达式里的if-else是"选值";多层for从左往右读、等价嵌套循环。 - 可读性优先:单
for+ 一个if随便写,三for以上改回循环;生成式通常比手写循环更快。 - 元组
():不可变;单元素必须写逗号(x,);支持拆包,交换变量a, b = b, a。 - 列表 vs 元组:可变用列表,定下来用元组;元组可哈希能做字典 key。
- 类型转换:
int/float/str/list/tuple/set;统计函数len/max/min/sum通用。
扩展阅读
- 官方文档 · 列表类型:https://docs.python.org/3/library/stdtypes.html#list
- 官方教程 · 数据结构(列表/元组):https://docs.python.org/3/tutorial/datastructures.html