3、Python基础语法(二):列表、元组、字典、集合的创建与常用方法、推导式、切片与遍历技巧

在Python中,列表(list)、元组(tuple)、字典(dict)和集合(set)是四种最核心的内置容器类型。它们决定了数据如何被组织、访问和传递。如果说变量是存储单个值的盒子,那么容器类型就是能够容纳多个值的“书架”。理解这四种数据结构的本质区别——尤其是它们的可变性(mutable vs immutable)、有序性(ordered vs unordered)以及唯一性(unique vs duplicate)——是写出高效、健壮Python代码的基石。本章将深入剖析这四种容器的创建方式、常用方法、推导式语法、切片操作与遍历技巧,并通过对比表格和实战案例,帮助你建立清晰的数据结构选型思维。

Python 四大容器类型核心特性对比 列表 (list) ✅ 有序 ✅ 可变 ✅ 允许重复 [1, 2, 3, 2] 元组 (tuple) ✅ 有序 ❌ 不可变 ✅ 允许重复 (1, 2, 3, 2) 字典 (dict) ✅ 有序 (3.7+) ✅ 可变 🔑 Key 唯一 {"a": 1, "b": 2} 集合 (set) ❌ 无序 ✅ 可变 🚫 元素唯一 {1, 2, 3} 📌 选型决策路径 需要键值映射? 是 → 字典 否 ↓ 需要去重? 是 → 集合 否 ↓ 需要修改? 是 → 列表 否 → 元组 注:Python 3.7+ 字典保持插入顺序;集合元素必须可哈希(hashable)

上图从“有序性、可变性、唯一性”三个维度对这四种容器进行了高度概括。接下来,我们将逐一拆解每种数据结构的创建语法、核心方法、适用场景,并重点讲解推导式与切片这两个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)。当数据量达到数万级别时,使用集合进行成员检查的性能优势非常明显。

总结复盘: 集合虽然不能存储重复元素,但它的去重能力和数学运算特性使其在数据处理(如日志分析、标签系统)中不可或缺。记住:需要去重或集合运算时,优先考虑 set。

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 遍历。它展示了如何将本章的各个知识点组合起来解决实际问题。

本章总结复盘: 列表、元组、字典、集合是Python数据处理的四大基石。选择哪种容器,取决于你对有序性、可变性、唯一性的需求。推导式是提升代码简洁度的利器,但需注意可读性;切片是处理序列数据的瑞士军刀;而 enumerate 和 zip 则是遍历时的最佳拍档。掌握这些工具,你将能够以更少的代码、更高的效率处理复杂的数据结构。