3、Python基础语法(二):列表、元组、字典、集合的创建与常用方法、推导式、切片与遍历技巧
在Python中,列表(list)、元组(tuple)、字典(dict)和集合(set)是四种最核心的内置容器类型。它们决定了数据如何被组织、访问和传递。如果说变量是存储单个值的盒子,那么容器类型就是能够容纳多个值的“书架”。理解这四种数据结构的本质区别——尤其是它们的可变性(mutable vs immutable)、有序性(ordered vs unordered)以及唯一性(unique vs duplicate)——是写出高效、健壮Python代码的基石。本章将深入剖析这四种容器的创建方式、常用方法、推导式语法、切片操作与遍历技巧,并通过对比表格和实战案例,帮助你建立清晰的数据结构选型思维。
上图从“有序性、可变性、唯一性”三个维度对这四种容器进行了高度概括。接下来,我们将逐一拆解每种数据结构的创建语法、核心方法、适用场景,并重点讲解推导式与切片这两个Python独有的高效语法糖。
3.1 列表(List):最灵活的有序序列
3.1.1 核心原理:动态数组与引用存储
列表在底层实现上是一个动态数组,它存储的是对象的引用(即内存地址),而非对象本身。这意味着列表可以混合存放任意类型的对象(整数、字符串、甚至另一个列表)。列表是可变(mutable)的,即我们可以通过索引直接修改元素、追加元素或删除元素,而无需创建新的列表对象。
3.1.2 创建与常用方法
创建列表有两种常见方式:直接使用方括号 [] 或使用内置函数 list()。以下表格总结了列表最常用的方法:
| 方法 | 描述 | 示例 | 时间复杂度 |
|---|---|---|---|
append(x) | 在末尾添加一个元素 | lst.append(5) | O(1) |
extend(iterable) | 在末尾一次性追加多个元素 | lst.extend([6,7]) | O(k) |
insert(i, x) | 在指定索引位置插入元素 | lst.insert(0, 'a') | O(n) |
remove(x) | 删除第一个值为x的元素 | lst.remove('a') | O(n) |
pop([i]) | 删除并返回索引i处的元素(默认末尾) | lst.pop() | O(1) / O(n) |
index(x) | 返回第一个值为x的索引 | lst.index(5) | O(n) |
sort() | 原地排序(默认升序) | lst.sort(reverse=True) | O(n log n) |
reverse() | 原地反转列表 | lst.reverse() | O(n) |
collections.deque(双端队列),因为列表的 insert(0, x) 和 pop(0) 的时间复杂度为 O(n),在数据量大时性能会急剧下降。
3.1.3 案例拆解:使用列表模拟一个简单的待办事项管理器
假设我们需要管理一个每日任务清单,要求支持添加任务、标记完成(删除)、查看所有任务。我们使用列表来存储任务字符串,并利用其方法实现核心逻辑:
# 初始化任务列表
tasks = []
# 添加任务
tasks.append("完成Python作业")
tasks.append("阅读技术文档")
tasks.append("锻炼30分钟")
# 打印当前任务
print("今日任务:", tasks)
# 模拟完成第一个任务(按索引删除)
completed = tasks.pop(0)
print(f"已完成:{completed},剩余任务:{tasks}")
# 在指定位置插入紧急任务
tasks.insert(1, "回复重要邮件")
print("更新后任务:", tasks)
# 检查任务是否存在
if "锻炼30分钟" in tasks:
print("锻炼任务还在列表中")
这个案例展示了 append、pop、insert 和成员运算符 in 的配合使用。列表的灵活性使其成为处理有序、可变数据的首选。
list = [] 作为函数默认参数。因为默认参数在函数定义时只被创建一次,后续调用会共享同一个列表,导致数据污染。正确做法是使用 def func(lst=None): if lst is None: lst = []。
3.2 元组(Tuple):不可变的安全序列
3.2.1 核心原理:不可变性与哈希能力
元组与列表在结构上几乎一致,但最本质的区别在于元组是不可变(immutable)的。一旦创建,就不能修改、添加或删除元素。这种特性带来了两个关键优势:线程安全(多线程环境下无需加锁)和可哈希性(元组可以作为字典的键或集合的元素,而列表则不行)。
3.2.2 创建与常用操作
创建元组通常使用圆括号 (),但要注意:单个元素的元组必须加逗号,例如 (1,),否则 (1) 会被解释为整数1。元组的方法较少,因为不可变,所以只有 count() 和 index() 两个方法。
# 创建元组
point = (10, 20)
single = (5,) # 必须加逗号
nested = (1, (2, 3), [4, 5]) # 元组内可以包含可变对象(但该可变对象本身可修改)
# 访问与解包
x, y = point
print(f"坐标:({x}, {y})")
# 元组不可变,但若包含列表,列表内容可变
nested[2].append(6)
print(nested) # 输出: (1, (2, 3), [4, 5, 6])
3.2.3 实战场景:函数多返回值与数据保护
元组最常见的应用场景是函数返回多个值。例如,一个函数需要返回商和余数:
def divmod_custom(a, b):
return a // b, a % b # 返回元组
quotient, remainder = divmod_custom(17, 5)
print(f"商:{quotient},余数:{remainder}")
此外,当我们需要传递一组不应被修改的数据(如数据库连接配置、坐标点)时,使用元组可以防止意外修改,提高代码的健壮性。
3.3 字典(Dict):键值映射的利器
3.3.1 核心原理:哈希表与散列冲突
字典是基于哈希表(Hash Table)实现的。当我们存储一个键值对 key: value 时,Python会计算 key 的哈希值,并通过哈希函数映射到内存中的某个位置。因此,字典的查找、插入和删除操作的平均时间复杂度均为 O(1)。需要注意的是,字典的 key 必须是可哈希的(即不可变类型,如字符串、数字、元组),而 value 可以是任意类型。
3.3.2 创建与常用方法
字典的创建方式灵活多样,包括花括号 {}、dict() 构造函数以及 fromkeys() 方法。以下表格列出了高频方法:
| 方法 | 描述 | 示例 |
|---|---|---|
get(key, default) | 获取键对应的值,键不存在时返回默认值(不报错) | d.get('name', '未知') |
keys() | 返回所有键的视图 | d.keys() |
values() | 返回所有值的视图 | d.values() |
items() | 返回所有键值对的视图 | d.items() |
update(dict2) | 合并另一个字典(覆盖相同键) | d.update({'a': 1}) |
pop(key, default) | 删除并返回指定键的值 | d.pop('a') |
setdefault(key, default) | 若键存在则返回其值,否则插入键并设置默认值 | d.setdefault('b', 0) |
3.3.3 案例拆解:使用字典统计文本中单词出现的频率
这是一个经典的数据统计场景。我们读取一段英文文本,统计每个单词出现的次数,并按频率降序输出前三个单词。
text = "the quick brown fox jumps over the lazy dog the quick"
words = text.split()
freq = {}
for word in words:
# 使用 get 方法实现计数,避免 KeyError
freq[word] = freq.get(word, 0) + 1
# 按值排序,取前3
top3 = sorted(freq.items(), key=lambda item: item[1], reverse=True)[:3]
print("词频统计Top3:", top3)
# 使用 setdefault 的等价写法
freq2 = {}
for word in words:
freq2[word] = freq2.setdefault(word, 0) + 1
print("freq2 结果一致:", freq2 == freq)
这个案例展示了 get() 方法在计数场景中的优雅用法,避免了繁琐的 if key in dict 判断。同时,sorted() 配合 lambda 函数实现了对字典按值排序。
RuntimeError: dictionary changed size during iteration。如果需要筛选,应遍历 list(d.items()) 的副本。
3.4 集合(Set):无序且唯一的数学集合
3.4.1 核心原理:哈希集合与去重
集合与字典的底层实现类似,也是基于哈希表,但集合只存储键而不存储值。集合中的元素必须是可哈希的且唯一。集合的主要用途包括:去重、成员关系测试(in 操作)以及数学集合运算(并集、交集、差集)。
3.4.2 创建与常用操作
创建集合使用花括号 {} 或 set() 函数。注意:空集合必须使用 set(),因为 {} 创建的是空字典。集合的常用操作如下:
# 创建集合
s1 = {1, 2, 3, 3, 2} # 自动去重,结果为 {1, 2, 3}
s2 = set([2, 3, 4])
# 集合运算
print("并集:", s1 | s2) # {1, 2, 3, 4}
print("交集:", s1 & s2) # {2, 3}
print("差集:", s1 - s2) # {1}
print("对称差集:", s1 ^ s2) # {1, 4}
# 添加与删除
s1.add(5)
s1.discard(10) # 删除不存在的元素不会报错
s1.remove(1) # 删除不存在的元素会抛出 KeyError
3.4.3 实战场景:数据去重与快速成员检查
假设我们有一个用户ID列表,其中包含重复项,我们需要找出所有唯一的用户ID,并快速判断某个ID是否在活跃用户列表中。
user_ids = [101, 102, 103, 101, 104, 102]
unique_ids = set(user_ids)
print("唯一用户ID:", unique_ids)
# 快速成员检查(O(1) 时间复杂度)
active_ids = {101, 103, 105}
for uid in unique_ids:
if uid in active_ids:
print(f"用户 {uid} 活跃")
集合的 in 操作时间复杂度为 O(1),而列表的 in 操作是 O(n)。当数据量达到数万级别时,使用集合进行成员检查的性能优势非常明显。
3.5 推导式(Comprehensions):一行代码构建容器
3.5.1 核心原理:声明式编程范式
推导式是Python提供的一种从可迭代对象(如列表、元组、字符串、range)快速构建新容器的语法糖。它基于声明式编程思想,让你专注于“做什么”而不是“怎么做”。推导式支持 for 循环和 if 条件过滤,可以应用于列表、字典和集合。
3.5.2 四种推导式语法对比
| 类型 | 语法结构 | 示例 | 结果 |
|---|---|---|---|
| 列表推导式 | [expr for item in iterable if condition] | [x**2 for x in range(5) if x % 2 == 0] | [0, 4, 16] |
| 字典推导式 | {key_expr: value_expr for item in iterable} | {x: x**2 for x in range(3)} | {0: 0, 1: 1, 2: 4} |
| 集合推导式 | {expr for item in iterable if condition} | {x % 3 for x in range(10)} | {0, 1, 2} |
| 生成器推导式 | (expr for item in iterable) | (x for x in range(100)) | 生成器对象(惰性求值) |
3.5.3 案例拆解:使用推导式清洗数据
假设我们有一个包含混合类型数据的列表,需要提取所有整数并计算其平方,同时过滤掉负数。使用传统循环和推导式对比:
data = [1, -2, '3', 4.5, 6, -7, '8']
# 传统方式
result = []
for item in data:
if isinstance(item, int) and item > 0:
result.append(item ** 2)
print("传统方式:", result)
# 推导式方式(更简洁)
result_comp = [item ** 2 for item in data if isinstance(item, int) and item > 0]
print("推导式:", result_comp)
推导式不仅代码更短,而且执行效率通常略高于等价的 for 循环,因为它在底层进行了优化。但要注意,推导式不适合包含复杂逻辑(如多个分支、异常处理)的场景,此时应使用普通循环以保持可读性。
3.6 切片(Slicing)与遍历技巧
3.6.1 切片操作:灵活截取子序列
切片是Python序列类型(列表、元组、字符串)共有的特性,语法为 seq[start:stop:step]。其中 start 是起始索引(包含),stop 是结束索引(不包含),step 是步长(默认为1)。切片返回的是新对象(浅拷贝)。
lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 基本切片
print(lst[2:5]) # [2, 3, 4]
print(lst[:4]) # [0, 1, 2, 3] (从头开始)
print(lst[5:]) # [5, 6, 7, 8, 9] (到末尾)
print(lst[::2]) # [0, 2, 4, 6, 8] (步长为2)
# 负数索引
print(lst[-3:]) # [7, 8, 9] (最后三个)
print(lst[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] (反转)
# 切片赋值(仅列表支持)
lst[1:3] = [20, 30]
print(lst) # [0, 20, 30, 3, 4, 5, 6, 7, 8, 9]
3.6.2 遍历技巧:enumerate 与 zip
在遍历容器时,我们经常需要同时获取索引和值,或者并行遍历多个容器。Python提供了两个内置函数来简化这些操作:
enumerate(iterable, start=0):返回索引和值的元组序列。zip(iterable1, iterable2, ...):将多个可迭代对象中对应的元素打包成元组。
# enumerate 示例
names = ['Alice', 'Bob', 'Charlie']
for idx, name in enumerate(names, start=1):
print(f"第{idx}位:{name}")
# zip 示例
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name} 的分数是 {score}")
# 解压 zip 对象
pairs = list(zip(names, scores))
print(pairs) # [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
3.6.3 案例拆解:使用切片与遍历实现数据分页
假设我们有一个包含100个商品ID的列表,需要每页显示10个商品,并打印每页的ID列表。利用切片可以轻松实现分页逻辑:
product_ids = list(range(1, 101)) # 1~100
page_size = 10
total_pages = (len(product_ids) + page_size - 1) // page_size # 向上取整
for page in range(total_pages):
start = page * page_size
end = start + page_size
page_items = product_ids[start:end]
print(f"第 {page+1} 页:{page_items}")
这个案例展示了切片在数据分页中的高效应用。相比使用循环加条件判断,切片代码更直观且不易出错。
lst = [[1], [2]]; sub = lst[:]; sub[0].append(99) 会导致 lst 变为 [[1, 99], [2]]。若需要深拷贝,应使用 copy.deepcopy()。
3.7 综合实战:构建一个简易的学生成绩管理系统
为了将本章所有知识点串联起来,我们设计一个综合案例:使用字典存储学生信息(姓名、成绩列表),利用列表推导式计算平均分,使用集合找出所有课程都及格的学生,并通过切片实现成绩排名展示。
# 数据准备:使用字典推导式创建学生成绩表
students = {
f"Student_{i}": [random.randint(50, 100) for _ in range(3)]
for i in range(1, 6)
}
# 假设已导入 random 模块
# 1. 使用列表推导式计算每个学生的平均分
averages = {name: sum(scores) / len(scores) for name, scores in students.items()}
print("平均分:", averages)
# 2. 使用集合找出所有成绩都 >= 60 的学生
passed_all = {name for name, scores in students.items() if all(s >= 60 for s in scores)}
print("全部及格的学生:", passed_all)
# 3. 按平均分降序排序,并取前3名(使用切片)
top3 = sorted(averages.items(), key=lambda x: x[1], reverse=True)[:3]
print("前三名:", top3)
# 4. 使用 enumerate 打印排名
for rank, (name, avg) in enumerate(top3, start=1):
print(f"第{rank}名:{name},平均分 {avg:.2f}")
这个案例综合运用了字典推导式、集合推导式、all() 函数、sorted() 排序、切片以及 enumerate 遍历。它展示了如何将本章的各个知识点组合起来解决实际问题。
enumerate 和 zip 则是遍历时的最佳拍档。掌握这些工具,你将能够以更少的代码、更高的效率处理复杂的数据结构。