📊 阅读时长: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 的时间几乎是恒定的。

这背后的魔法是哈希表

  1. 当你写 d['name'] 时,Python 对 'name' 这个字符串计算一个哈希值(一个整数)
  2. 用这个哈希值快速定位到存储位置
  3. 直接取出对应的 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

为什么这个区别很重要?

  1. 赋值行为不同:可变对象赋值是共享引用,不可变对象赋值是"创建新对象"的假象
  2. 作为函数参数时:可变对象在函数内部修改会影响外部,不可变对象不会
  3. 作为字典 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 篇。关注我,不错过后续更新。

更多推荐