【Python系列课程】Python六大标准数据类型详解(下):列表、元组、字典、集合
📊 阅读时长:22分钟 | 关键词:Python列表、元组、字典、集合、可变对象、哈希表、集合运算
引言:四种容器,四种哲学
上篇文章我们讲了数字和字符串——它们都是不可变的。创建之后就不能修改,任何"修改"操作都是在创建新对象。
今天要讲的四种容器类型,情况就复杂多了。它们有的可变、有的不可变,有的有序、有的无序,有的能存重复元素、有的不能。这些差异不是随机的——每种容器类型都是为特定使用场景设计的。
搞清楚它们之间的区别和各自的使用场景,是用好 Python 最关键的一步。
先上一张全景对比表,你可以把它当作速查手册:
| 特性 | List(列表) | Tuple(元组) | Dict(字典) | Set(集合) |
|---|---|---|---|---|
| 创建语法 | [1, 2, 3] |
(1, 2, 3) |
{'a':1, 'b':2} |
{1, 2, 3} |
| 可变性 | ✅ 可变 | ❌ 不可变 | ✅ 可变 | ✅ 可变 |
| 有序性 | ✅ 有序 | ✅ 有序 | ✅ 有序(3.7+) | ❌ 无序 |
| 允许重复 | ✅ 允许 | ✅ 允许 | Key 不能重复 | ❌ 不允许 |
| 访问方式 | 索引 [0] |
索引 [0] |
Key ['a'] |
不可索引 |
| 底层实现 | 动态数组 | 静态数组 | 哈希表 | 哈希表 |
| 内存占用 | 较大 | 较小 | 较大 | 中等 |
| 查找速度 | O(n) | O(n) | O(1) | O(1) |
| 典型场景 | 需要增删改的数据集 | 不希望被修改的数据 | 键值对映射 | 去重、集合运算 |

一、列表(List):最万能的容器
列表是 Python 中使用频率最高的容器类型,没有之一。它的设计哲学是:给你最大的灵活性。
1.1 列表的创建方式
# 方式一:直接写
fruits = ['apple', 'banana', 'orange']
# 方式二:用 list() 构造函数
chars = list('hello') # ['h', 'e', 'l', 'l', 'o']
nums = list(range(5)) # [0, 1, 2, 3, 4]
# 方式三:列表推导式(后面会详细讲)
squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
# 列表的灵活性:可以混合不同类型
mixed = [1, 'hello', 3.14, True, None, [1, 2, 3]]
# 整数 字符串 浮点 布尔 空值 嵌套列表
空列表:[] 或 list()。
1.2 索引和切片 —— 和字符串一模一样
列表的索引和切片规则与字符串完全一致:
nums = [10, 20, 30, 40, 50]
# 正索引: 0 1 2 3 4
# 负索引: -5 -4 -3 -2 -1
print(nums[0]) # 10 —— 第一个
print(nums[-1]) # 50 —— 最后一个
print(nums[1:3]) # [20, 30] —— 索引 1,2(左闭右开)
print(nums[:3]) # [10, 20, 30] —— 前三个
print(nums[2:]) # [30, 40, 50] —— 从索引 2 到末尾
print(nums[::2]) # [10, 30, 50] —— 步长为 2
print(nums[::-1]) # [50, 40, 30, 20, 10] —— 反转
1.3 增删改查 —— 列表的核心操作
这是日常使用最多的操作,必须熟练掌握。
增加元素:
nums = [1, 2, 3]
# append() —— 在末尾追加一个元素(最常用)
nums.append(4)
print(nums) # [1, 2, 3, 4]
# insert() —— 在指定位置插入
nums.insert(1, 99) # 在索引 1 处插入 99
print(nums) # [1, 99, 2, 3, 4]
# extend() —— 合并另一个列表(批量追加)
nums.extend([5, 6, 7])
print(nums) # [1, 99, 2, 3, 4, 5, 6, 7]
# 注意:append([5,6]) 和 extend([5,6]) 的区别
test = [1, 2]
test.append([3, 4]) # [1, 2, [3, 4]] —— 把整个列表作为一个元素加进去
test = [1, 2]
test.extend([3, 4]) # [1, 2, 3, 4] —— 把列表中的元素逐个加进去

删除元素:
nums = [10, 20, 30, 40, 50, 30]
# pop() —— 按索引删除,返回被删除的值
last = nums.pop() # 删除并返回最后一个 → 30
print(nums) # [10, 20, 30, 40, 50]
second = nums.pop(1) # 删除并返回索引 1 的元素 → 20
print(nums) # [10, 30, 40, 50]
# remove() —— 按值删除(只删第一个匹配的)
nums.remove(30) # 删除第一个值为 30 的元素
print(nums) # [10, 40, 50]
# del —— 按索引删除(不返回值)
del nums[0] # 删除索引 0 的元素
print(nums) # [40, 50]
# clear() —— 清空整个列表
nums.clear()
print(nums) # []
修改元素:
nums = [10, 20, 30]
# 单个修改
nums[0] = 999
print(nums) # [999, 20, 30]
# 批量修改(切片赋值)
nums[1:3] = [88, 99]
print(nums) # [999, 88, 99]
# 注意:切片赋值的元素数量可以和原切片不同
nums[0:2] = [1, 2, 3, 4]
print(nums) # [1, 2, 3, 4, 99] —— 替换了 2 个,插入了 4 个
查找元素:
nums = [10, 20, 30, 20, 40]
# in / not in —— 判断元素是否存在
print(20 in nums) # True
print(50 in nums) # False
print(50 not in nums) # True
# index() —— 查找元素位置(只返回第一个匹配的)
print(nums.index(20)) # 1
print(nums.index(20, 2)) # 3 —— 从索引 2 开始找
# nums.index(50) # ValueError! 不存在的元素会报错
# count() —— 统计出现次数
print(nums.count(20)) # 2
# len() —— 列表长度
print(len(nums)) # 5
1.4 列表的排序和反转
nums = [3, 1, 4, 1, 5, 9, 2]
# sorted() —— 返回新列表,原列表不变
sorted_nums = sorted(nums)
print(sorted_nums) # [1, 1, 2, 3, 4, 5, 9]
print(nums) # [3, 1, 4, 1, 5, 9, 2] —— 原列表不变
# sort() —— 原地排序,修改原列表
nums.sort()
print(nums) # [1, 1, 2, 3, 4, 5, 9]
# 降序排列
nums.sort(reverse=True)
print(nums) # [9, 5, 4, 3, 2, 1, 1]
# reverse() —— 反转列表
nums.reverse()
print(nums) # [1, 1, 2, 3, 4, 5, 9] —— 只是反转,不是排序
# 自定义排序规则
words = ['apple', 'Banana', 'cherry', 'Date']
words.sort() # 默认按 ASCII 排序(大写字母在前)
print(words) # ['Banana', 'Date', 'apple', 'cherry']
words.sort(key=str.lower) # 忽略大小写排序
print(words) # ['apple', 'Banana', 'cherry', 'Date']
words.sort(key=len) # 按长度排序
print(words) # ['Date', 'apple', 'Banana', 'cherry']
1.5 ⚠️ 列表的"可变性"陷阱 —— 这是新手翻车最多的地方
a = [1, 2, 3]
b = a # 你以为复制了一份?错!b 和 a 指向同一个列表
b.append(4)
print(a) # [1, 2, 3, 4] —— a 也被改了!
print(b) # [1, 2, 3, 4]
为什么会这样?因为在 Python 中,b = a 只是让 b 也指向 a 指向的那个列表对象。它们指向的是同一块内存。

如何创建真正的副本?
a = [1, 2, 3]
# 方式一:copy() 方法
b = a.copy()
# 方式二:切片 [:]
b = a[:]
# 方式三:list() 构造函数
b = list(a)
# 验证:修改 b 不会影响 a
b.append(4)
print(a) # [1, 2, 3] —— a 不受影响 ✓
print(b) # [1, 2, 3, 4]
⚠️ 浅拷贝 vs 深拷贝:上面的三种方式都是浅拷贝——如果列表中包含嵌套列表,内层列表仍然是共享的!
import copy
original = [[1, 2], [3, 4]]
# 浅拷贝:外层是新的,内层还是原来的
shallow = original.copy()
shallow[0][0] = 999
print(original) # [[999, 2], [3, 4]] —— 内层被改了!
# 深拷贝:完完全全独立
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 999
print(original) # [[1, 2], [3, 4]] —— 完全不受影响 ✓
什么时候用深拷贝? 当你的列表包含嵌套的可变对象(列表、字典等),且你需要完全独立的副本时。
1.6 列表推导式 —— Python 的优雅表达
列表推导式是 Python 最受欢迎的特性之一,让你用一行代码完成"遍历 + 过滤 + 转换":
# 传统写法:创建一个平方数列表
squares = []
for x in range(10):
squares.append(x**2)
# 列表推导式:一行搞定
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 加过滤条件:只保留偶数的平方
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# 嵌套循环:生成坐标点
points = [(x, y) for x in range(3) for y in range(3)]
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]
# 结合条件表达式
labels = ['偶数' if x % 2 == 0 else '奇数' for x in range(5)]
# ['偶数', '奇数', '偶数', '奇数', '偶数']
二、元组(Tuple):不可变的列表
元组和列表几乎一模一样,除了一个关键区别——元组不可修改。
2.1 元组的创建
# 基本创建
point = (3, 4)
colors = ('red', 'green', 'blue')
# 单元素元组:必须有逗号!(这是最容易犯的错)
single = (1,) # 这是元组
not_tuple = (1) # 这不是元组,只是数字 1 加括号!
print(type(single)) # <class 'tuple'>
print(type(not_tuple)) # <class 'int'>
# 括号可以省略(但不推荐,可读性差)
point = 3, 4 # 这也是元组
print(type(point)) # <class 'tuple'>
# 空元组
empty = ()
2.2 元组不可变 —— 强行修改会怎样?
point = (3, 4)
# 读取:没问题
print(point[0]) # 3
print(point[1]) # 4
# 修改:报错!
point[0] = 5 # TypeError: 'tuple' object does not support item assignment
# 删除元素:报错!
del point[0] # TypeError
# 也没有 append()、remove() 等方法
但注意:如果元组中包含可变对象(如列表),那个可变对象的内容是可以改的:
t = (1, [2, 3], 4)
t[1][0] = 999 # 修改元组中的列表元素——这是允许的!
print(t) # (1, [999, 3], 4)
因为元组的"不可变"指的是元组本身的元素引用不可变,而不是引用指向的对象不可变。
2.3 什么时候用元组?
| 场景 | 为什么用元组 | 示例 |
|---|---|---|
| 函数返回多个值 | Python 自动打包成元组 | return x, y, z |
| 不希望被修改的数据 | 提供"写保护" | 配置常量、坐标点、RGB 颜色值 |
| 作为字典的键 | 元组可哈希,列表不能 | cache[(x, y)] = value |
| 性能敏感场景 | 元组比列表更轻量 | 大量只读数据的存储 |
函数返回多个值的经典用法:
def get_user_info():
# 看似返回三个值,实际返回的是一个元组
return '小明', 25, '北京'
# 自动解包
name, age, city = get_user_info()
print(name, age, city) # 小明 25 北京
# 也可以先接收元组再处理
info = get_user_info()
print(info) # ('小明', 25, '北京')
print(info[0]) # '小明'
2.4 元组和列表的方法对比
元组只有两个方法(因为它不可变,所以没有增删改的方法):
t = (1, 2, 3, 2, 4, 2)
print(t.count(2)) # 3 —— 统计 2 出现的次数
print(t.index(3)) # 2 —— 查找 3 的位置
| 操作 | 列表 | 元组 |
|---|---|---|
索引 [i] |
✅ | ✅ |
切片 [a:b] |
✅ | ✅ |
len() |
✅ | ✅ |
in 成员检查 |
✅ | ✅ |
count() |
✅ | ✅ |
index() |
✅ | ✅ |
append() |
✅ | ❌ |
insert() |
✅ | ❌ |
remove() |
✅ | ❌ |
pop() |
✅ | ❌ |
sort() |
✅ | ❌ |
三、字典(Dictionary):键值对的王者
字典是 Python 中最强大的数据结构之一。它底层基于哈希表实现,查找速度极快——不管字典有多大,根据 key 查找 value 的时间几乎是恒定的。
3.1 字典的创建
# 方式一:花括号
user = {
'name': '小明',
'age': 25,
'city': '北京'
}
# 方式二:dict() 构造函数
user = dict(name='小明', age=25, city='北京')
# 方式三:从键值对序列创建
user = dict([('name', '小明'), ('age', 25), ('city', '北京')])
# 方式四:字典推导式
squares = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 空字典
empty = {}
3.2 字典的基本操作(CRUD)
user = {'name': '小明', 'age': 25}
# ===== 访问(Read)=====
print(user['name']) # '小明'
# print(user['gender']) # KeyError! 不存在的 key 直接报错
# 安全访问:get() 方法
print(user.get('gender')) # None —— 不存在返回 None,不报错
print(user.get('gender', '未知')) # '未知' —— 可指定默认值
# ===== 增加/修改(Create/Update)=====
user['email'] = 'xiaoming@example.com' # 新增
user['age'] = 26 # 修改
print(user)
# {'name': '小明', 'age': 26, 'email': 'xiaoming@example.com'}
# update() —— 批量更新
user.update({'city': '北京', 'phone': '138xxxx'})
print(user)
# {'name': '小明', 'age': 26, 'email': '...', 'city': '北京', 'phone': '138xxxx'}
# setdefault() —— 如果 key 不存在才设置(不覆盖已有值)
user.setdefault('age', 30) # age 已存在,不修改 → 26
user.setdefault('gender', '男') # gender 不存在,新增
print(user['gender']) # '男'
# ===== 删除(Delete)=====
del user['phone'] # 删除键值对
email = user.pop('email') # 删除并返回值
print(email) # 'xiaoming@example.com'
last = user.popitem() # 删除并返回最后一个键值对(3.7+)
print(last) # ('gender', '男')
user.clear() # 清空字典
3.3 字典的遍历
user = {'name': '小明', 'age': 25, 'city': '北京'}
# 遍历 key(默认)
for key in user:
print(key, user[key])
# 遍历 values
for value in user.values():
print(value)
# 遍历 key-value 对(最常用)
for key, value in user.items():
print(f'{key}: {value}')
# 输出:
# name: 小明
# age: 25
# city: 北京
3.4 字典的常用方法速查
d = {'a': 1, 'b': 2, 'c': 3}
# 获取所有 key
print(d.keys()) # dict_keys(['a', 'b', 'c'])
# 获取所有 value
print(d.values()) # dict_values([1, 2, 3])
# 获取所有 key-value 对
print(d.items()) # dict_items([('a', 1), ('b', 2), ('c', 3)])
# 检查 key 是否存在
print('a' in d) # True
print('z' in d) # False
print('z' not in d) # True
# 长度
print(len(d)) # 3
# 复制
d2 = d.copy() # 浅拷贝
3.5 为什么字典这么快?哈希表原理简述
字典的查找速度是 O(1)——意思是,不管字典里有 10 个键值对还是 1000 万个,查找一个 key 的时间几乎是恒定的。
这背后的魔法是哈希表:
- 当你写
d['name']时,Python 对'name'这个字符串计算一个哈希值(一个整数) - 用这个哈希值快速定位到存储位置
- 直接取出对应的 value

这也解释了为什么字典的 key 必须是不可变对象——因为可变对象的内容会变,哈希值也会变,那之前存储的位置就找不到了。
# 可以用作 key 的类型(不可变,可哈希)
d = {}
d['name'] = '小明' # str ✓
d[123] = '数字' # int ✓
d[(1, 2)] = '坐标' # tuple ✓
# 不能用作 key 的类型(可变,不可哈希)
# d[[1, 2]] = '列表' # TypeError! list 不可哈希
# d[{'a': 1}] = '字典' # TypeError! dict 不可哈希
四、集合(Set):去重和集合运算
集合有两个核心用途:去重和数学集合运算。
4.1 集合的创建
# 方式一:花括号(注意:空花括号 {} 是字典,不是集合!)
fruits = {'apple', 'banana', 'orange'}
# 方式二:set() 构造函数
chars = set('hello') # {'h', 'e', 'l', 'o'} —— 自动去重,'l' 只保留一个
nums = set([1, 2, 3, 2, 1]) # {1, 2, 3} —— 自动去重
# 空集合:只能用 set(),不能用 {}
empty = set() # ✓ 空集合
empty_dict = {} # ✗ 这是空字典!
print(type(empty)) # <class 'set'>
print(type(empty_dict)) # <class 'dict'>
4.2 集合的特性
s = {3, 1, 4, 1, 5, 9, 2}
# 1. 自动去重
print(s) # {1, 2, 3, 4, 5, 9} —— 重复的 1 被去掉了
# 2. 无序(输出顺序不固定)
print(s) # 每次运行可能不同顺序
# 3. 不能通过索引访问
# s[0] # TypeError! 'set' object is not subscriptable
# 4. 元素必须是不可变类型
valid = {1, 'hello', (1, 2)} # ✓
# invalid = {1, [1, 2]} # TypeError! 列表不可哈希
4.3 集合的增删操作
s = {1, 2, 3}
# 添加
s.add(4) # {1, 2, 3, 4} —— 添加单个元素
s.add(2) # {1, 2, 3, 4} —— 已存在,无变化
# 批量添加
s.update([5, 6, 7]) # {1, 2, 3, 4, 5, 6, 7}
# 删除
s.remove(3) # 删除 3,如果不存在会报错 KeyError
s.discard(10) # 删除 10,如果不存在不报错(比 remove 安全)
item = s.pop() # 随机删除并返回一个元素(因为无序,所以是"随机")
s.clear() # 清空集合
4.4 集合运算 —— 数学中的并交差
这是集合最强大的功能,让你用简洁的运算符完成复杂的集合操作:
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}
# 并集(Union):在 a 或 b 中的元素
print(a | b) # {1, 2, 3, 4, 5, 6, 7, 8}
print(a.union(b)) # 同上
# 交集(Intersection):同时在 a 和 b 中的元素
print(a & b) # {4, 5}
print(a.intersection(b)) # 同上
# 差集(Difference):在 a 中但不在 b 中的元素
print(a - b) # {1, 2, 3}
print(a.difference(b)) # 同上
# 对称差集(Symmetric Difference):在 a 或 b 中但不同时在两者中
print(a ^ b) # {1, 2, 3, 6, 7, 8}
print(a.symmetric_difference(b)) # 同上

集合运算在实际中的应用:
# 示例:找出两个用户共同关注的人
user_a_follows = {'张三', '李四', '王五', '赵六'}
user_b_follows = {'王五', '赵六', '孙七', '周八'}
# 共同关注
common = user_a_follows & user_b_follows
print(f'共同关注:{common}') # {'王五', '赵六'}
# A 关注但 B 不关注的人(可以推荐给 B)
recommend_to_b = user_a_follows - user_b_follows
print(f'推荐给B:{recommend_to_b}') # {'张三', '李四'}
# 所有关注的人
all_follows = user_a_follows | user_b_follows
print(f'全部关注:{all_follows}') # 8 个人
4.5 集合的关系判断
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
c = {1, 2, 3}
# 子集判断
print(a.issubset(b)) # True —— a 是 b 的子集
print(a <= b) # True —— 同上,运算符形式
# 真子集(a 是 b 的子集且 a ≠ b)
print(a < b) # True
print(a < c) # False —— a 和 c 相等
# 超集判断
print(b.issuperset(a)) # True —— b 是 a 的超集
print(b >= a) # True
# 不相交(没有共同元素)
print(a.isdisjoint({7, 8, 9})) # True —— 没有交集
4.6 集合推导式
和列表推导式语法一样,只是把方括号换成花括号:
# 生成 0~9 中偶数的平方集合
squares = {x**2 for x in range(10) if x % 2 == 0}
# {0, 4, 16, 36, 64}
# 注意:结果是集合,所以无序且去重
五、可变 vs 不可变:一张图终结你的困惑
这是 Python 数据类型中最重要的概念,我用 id() 函数来直观展示:
# ===== 不可变类型:修改 = 创建新对象 =====
a = 'hello'
print(f'修改前 id: {id(a)}') # 例如 4378419440
a = a + ' world'
print(f'修改后 id: {id(a)}') # 例如 4378518832 —— 不同!新对象
# ===== 可变类型:修改 = 原地修改 =====
b = [1, 2, 3]
print(f'修改前 id: {id(b)}') # 例如 4378524352
b.append(4)
print(f'修改后 id: {id(b)}') # 例如 4378524352 —— 相同!原地修改
完整的可变性对照表:
不可变类型(Immutable) 可变类型(Mutable)
═══════════════════════════ ═══════════════════════
int, float, bool, complex list
str dict
tuple set
frozenset bytearray
bytes
为什么这个区别很重要?
- 赋值行为不同:可变对象赋值是共享引用,不可变对象赋值是"创建新对象"的假象
- 作为函数参数时:可变对象在函数内部修改会影响外部,不可变对象不会
- 作为字典 key 时:只有不可变对象可以
六、动手练习
练习 1:列表去重并保持顺序
# 题目:给定一个列表,去除重复元素但保持原有顺序
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# 方法一:用字典(3.7+ 保持插入顺序)
result = list(dict.fromkeys(nums))
print(result) # [3, 1, 4, 5, 9, 2, 6]
# 方法二:手动实现
seen = set()
result = []
for x in nums:
if x not in seen:
result.append(x)
seen.add(x)
print(result) # [3, 1, 4, 5, 9, 2, 6]
练习 2:统计词频
# 题目:统计一段文本中每个单词出现的次数
text = 'apple banana apple orange banana apple'
words = text.split()
freq = {}
for word in words:
freq[word] = freq.get(word, 0) + 1
print(freq) # {'apple': 3, 'banana': 2, 'orange': 1}
# 找出出现最多的单词
most_common = max(freq, key=freq.get)
print(f'出现最多的是: {most_common},共 {freq[most_common]} 次')
练习 3:集合运算实战
# 题目:两个班级的选课情况,找出各种统计信息
class_a = {'语文', '数学', '英语', '物理'}
class_b = {'数学', '英语', '化学', '生物'}
# 1. 两个班共开设了多少门课?
print(len(class_a | class_b)) # 6
# 2. 哪些课两个班都开了?
print(class_a & class_b) # {'数学', '英语'}
# 3. A 班有而 B 班没有的课?
print(class_a - class_b) # {'语文', '物理'}
小结
今天这篇文章信息量很大,四种容器类型各有各的使用场景。我们用一个决策流程图来帮你选择:
你需要存储一组数据 →
│
├─ 需要键值对映射?→ 用 dict(字典)
│
├─ 需要去重或集合运算?→ 用 set(集合)
│
├─ 数据不希望被修改?→ 用 tuple(元组)
│
└─ 需要频繁增删改?→ 用 list(列表)
核心要点回顾:
| 容器 | 一句话总结 | 最容易犯的错 |
|---|---|---|
| 列表 | 万能容器,需要频繁增删改时用它 | b = a 是共享引用,要用 b = a.copy() |
| 元组 | 不可变的列表,用于"写保护"场景 | 单元素元组忘记逗号 (1) 不是元组 |
| 字典 | 键值对映射,O(1) 查找速度 | 访问不存在的 key 会报错,用 get() 更安全 |
| 集合 | 去重 + 集合运算 | {} 是空字典不是空集合,空集合用 set() |
理解了可变与不可变的区别,你就掌握了 Python 数据类型设计的核心哲学。下一篇文章,我们将进入运算符和表达式——算术、比较、赋值、逻辑、成员、身份,看看 Python 如何用简洁的运算符让代码变得优雅。
本文是「Python从入门到数据分析」系列的第 3 篇,共 24 篇。关注我,不错过后续更新。
更多推荐
所有评论(0)