CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Python 语言基础/课时/列表字典与集合
RawBlame
课时目录12 节

Python 语言基础

  1. 01Python 是什么
  2. 02语法与缩进
  3. 03类型与变量
  4. 04条件与循环
  5. 05函数
  6. 06列表字典与集合
  7. 07字符串处理
  8. 08文件读写
  9. 09模块与 pip
  10. 10类与对象入门
  11. 11异常处理
  12. 12综合练习与下一步

列表字典与集合

list/dict/tuple/set 常用操作。

40 分钟

列表字典与集合

这一课你将能做到

  1. 熟练操作 list 的增删改查、排序、切片,并说清 tuple 与 list 的取舍
  2. 用 dict 完成键值存取、遍历、安全取值(get / in),写出统计词频这类高频小任务
  3. 用 set 完成去重与交并差运算,并根据「要不要有序、要不要去重」四象限选对容器

为什么要学

真实程序 80% 的时间在和数据结构打交道:爬回来的结果存列表、用户配置存字典、标签去重用集合、不该被改的坐标用元组。Java 里你得选 ArrayList、HashMap、HashSet;Python 里对应 list、dict、set,而且是内置类型,直接写 [] {} 就有。容器用不对,后面写文件、写类、做数据分析全是别扭的。

人话:list 是排队(有顺序、可插队);dict 是通讯录(名字查电话,速度快);set 是点名册(只记来没来过,不重复);tuple 是刻在石头上的名单(写完不许改)。

先建立图像

list   ["a","b","c"]     有序 · 可变 · 可重复     → 排队
tuple  ("a","b","c")     有序 · 不可变 · 可重复   → 石碑
dict   {"a":1,"b":2}     键值对 · 键唯一 · 3.7+保持插入序 → 通讯录
set    {"a","b"}         无序 · 可变 · 自动去重   → 点名册

四象限选型:

          有序              无序
可变     list              set
不可变   tuple             ——(frozenset 冷门)
需要「键找值」→ dict

跟着做

步骤 1:list 增删改查全流程

新建 collections_demo.py:

nums = [3, 1, 4, 1, 5]

# 查:下标从 0 开始,负数从尾数
print(nums[0], nums[-1])        # 3 5

# 增:append 尾加,insert 指定位置,extend 合并
nums.append(9)
nums.insert(0, 100)
nums.extend([2, 6])
print(nums)                     # [100, 3, 1, 4, 1, 5, 9, 2, 6]

# 删:remove 按值删第一个,pop 按下标删并返回,del 按下标删
nums.remove(100)
last = nums.pop()               # 删尾并返回 6
del nums[0]
print(nums, "被 pop 的:", last) # [3, 1, 4, 1, 5, 9, 2]

# 改:下标直接赋值
nums[0] = 33
print(nums[0])                  # 33

# 成员判断与长度
print(5 in nums, len(nums))     # True 7

输出:

3 5
[100, 3, 1, 4, 1, 5, 9, 2, 6]
[3, 1, 4, 1, 5, 9, 2] 被 pop 的: 6
33
True 7

步骤 2:排序与切片

scores = [88, 72, 95, 60]

print(sorted(scores))            # [60, 72, 88, 95]  返回新列表
print(scores)                    # [88, 72, 95, 60]  原列表没动
scores.sort(reverse=True)        # 原地降序,返回 None!
print(scores)                    # [95, 88, 72, 60]

words = ["banana", "fig", "apple"]
print(sorted(words, key=len))    # 按长度排:['fig', 'apple', 'banana']

# 切片 [start:stop:step],含头不含尾
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:4])     # [1, 2, 3]
print(nums[:3])      # [0, 1, 2]     前 3 个
print(nums[-2:])     # [4, 5]        最后 2 个
print(nums[::2])     # [0, 2, 4]     隔一个取一个
print(nums[::-1])    # [5, 4, 3, 2, 1, 0]  反转
copy = nums[:]       # 浅拷贝整个列表

输出:

[60, 72, 88, 95]
[88, 72, 95, 60]
[95, 88, 72, 60]
['fig', 'apple', 'banana']
[1, 2, 3]
[0, 1, 2]
[4, 5]
[0, 2, 4]
[5, 4, 3, 2, 1, 0]

重点区分:sorted() 返回新列表,.sort() 原地修改并返回 None——把 x = lst.sort() 当返回值接是经典 bug。

步骤 3:tuple——不可变序列

point = (3, 5)
x, y = point                    # 解包
print(x, y)                     # 3 5

# point[0] = 99                 # TypeError!元组不可修改
single = (42,)                  # 单元素元组必须带逗号
not_a_tuple = (42)              # 这只是 int 42
print(type(single), type(not_a_tuple))
# <class 'tuple'> <class 'int'>

输出:

3 5
<class 'tuple'> <class 'int'>

什么时候用 tuple:函数返回多个值、字典的键、坐标/RGB 这类「结构固定的一组值」。规则一句话:会改用 list,不改用 tuple。

步骤 4:dict——键值对核心操作

user = {"name": "小明", "age": 18}

# 取值:[] 键不存在会炸,get 给默认值不炸
print(user["name"])             # 小明
print(user.get("email"))        # None
print(user.get("email", "未填写"))  # 未填写

# 增/改:同一个方括号
user["email"] = "xm@test.com"   # 新键 = 增
user["age"] = 19                # 旧键 = 改
print(user)

# 删
age = user.pop("age")           # 删除并返回值
print(age, user)                # 19 {'name': '小明', 'email': 'xm@test.com'}

# 判断键是否存在用 in(判断的是键不是值)
print("name" in user)           # True

# 三种遍历
for k in user:                  # 默认遍历键
    print(k)
for k, v in user.items():       # 键值对一起拿(最常用)
    print(k, "=", v)

输出:

小明
None
未填写
{'name': '小明', 'age': 19, 'email': 'xm@test.com'}
19 {'name': '小明', 'email': 'xm@test.com'}
True
name
email
name = 小明
email = xm@test.com

实战——一行统计词频:

text = "a b a c a b"
freq = {}
for word in text.split():
    freq[word] = freq.get(word, 0) + 1
print(freq)                     # {'a': 3, 'b': 2, 'c': 1}

输出:

{'a': 3, 'b': 2, 'c': 1}

步骤 5:set——去重与集合运算

tags = ["python", "java", "python", "go", "java"]
unique = set(tags)
print(unique)                   # {'go', 'python', 'java'}(顺序不保证)
print(len(unique))              # 3

# 增删
unique.add("rust")
unique.discard("go")            # 不存在也不报错(remove 会报 KeyError)
print(unique)

# 交并差
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b)      # {3, 4}      交集
print(a | b)      # {1, 2, 3, 4, 5, 6}  并集
print(a - b)      # {1, 2}      差集:a 有 b 没有
print(a ^ b)      # {1, 2, 5, 6} 对称差:只属于一边的

输出:

{'go', 'python', 'java'}
3
{'rust', 'python', 'java'}
{3, 4}
{1, 2, 3, 4, 5, 6}
{1, 2}
{1, 2, 5, 6}

注意:空集合必须写 set()——{} 是空字典。

步骤 6:嵌套结构——真实数据长这样

students = [
    {"name": "小明", "scores": [88, 92]},
    {"name": "小红", "scores": [75, 81]},
]

for s in students:
    avg = sum(s["scores"]) / len(s["scores"])
    print(f"{s['name']} 平均分 {avg:.1f}")

输出:

小明 平均分 90.0
小红 平均分 78.0

JSON 解析出来的数据就是这种「字典套列表、列表套字典」的形态,练熟嵌套取值就等于提前练了 JSON。

讲清楚要点

  1. 可变性是容器的分水岭:list/dict/set 可变(原地修改影响所有引用);tuple/str 不可变(只能整体替换)。第 5 课「函数内改列表外面也变」的现象根源在此。
  2. dict 的键必须是不可变对象:str、int、tuple 可以当键,list 不行(TypeError: unhashable type: 'list')。因为 dict 靠 hash 定位键。
  3. dict 取值两派:d[k] 键不存在抛 KeyError;d.get(k, 默认值) 温和返回默认值。读配置、统计场景用 get,确认键一定存在用 [](错了就该炸出来提醒你)。
  4. 含头不含尾贯穿始终:切片 [1:4]、range(1, 4) 都取 1、2、3。记住这条,切片和 range 的 bug 少一半。
  5. in 对四类容器都适用:list/tuple 慢(逐个比),dict/set 快(hash 直达)。大数据量的成员判断一律换成 set。
  6. set 无序且元素必须不可变:不能 s[0] 取元素(没有下标),可变对象进不了 set。去重会丢掉原有顺序,要保序去重用 dict.fromkeys(lst)。
  7. 浅拷贝陷阱:b = a[:] 只拷贝外层;嵌套列表里的子列表仍共享。真正独立的拷贝用 copy.deepcopy(a)。

你可能会懵的地方

  • dict 到底有没有序? Python 3.7 起官方保证 dict 记住插入顺序,遍历按插入先后。但它不是排序——按值排序要自己 sorted(d.items(), key=...)。更早版本无序,老资料别看混。

  • 为什么 d["a"] += 1 有时报 KeyError? += 等价于先取后写:d["a"] = d["a"] + 1。键不存在时「取」这一步就炸了。首次累加用 d[k] = d.get(k, 0) + 1,或 collections.defaultdict(int)。

  • tuple 既然不可变,里面装个 list 呢? 元组本身不可变指的是「槽位不能换」,槽位里的可变对象内容仍能改:t = (1, [2, 3]) 后 t[1].append(4) 合法。这也是 tuple 不能当 dict 键的原因之一(可 hash 要求内容整体不可变)。

  • b = a 和 b = a[:] 差在哪? b = a 两个名字贴同一个列表,改一个「两个都变」;b = a[:] 是浅拷贝,外层独立、内层嵌套对象仍共享。想完全独立用 copy.deepcopy。

  • 什么时候用列表推导式,什么时候用循环? 简单转换/过滤用推导式(一行读完);逻辑超过两层嵌套、或者边遍历边有副作用(写文件、print 日志),用普通循环。推导式是优化可读性的,不是炫技的。

错法对照

错法问题正解
x = lst.sort()sort 返回 None,x 变 Nonelst.sort() 或 x = sorted(lst)
d[key] 直接取可能不存在的键KeyError 中断程序d.get(key, 默认值) 或先 in 判断
{} 当空集合用{} 是空字典空集合写 set()
用 list 当 dict 的键TypeError: unhashable换 tuple 或字符串
for x in lst 时 lst.remove(x)边遍历边删,跳元素遍历副本 for x in lst[:] 或列表推导式重建
b = a 以为是复制两个名字共享同一对象b = a[:](浅)或 copy.deepcopy(深)
单元素元组写 (42)得到 int 42(42,) 必须带逗号

卡点急救

  1. KeyError:dict 键不存在。用 get 带默认值,或 if k in d 先判断;统计累加场景用 d.get(k, 0) + 1。
  2. TypeError: unhashable type: 'list':把 list 用成了 dict 键或放进了 set。把内层改成 tuple:tuple(mylist)。
  3. IndexError: list index out of range:下标越界。先 len(lst) 看长度;负下标最小到 -len。
  4. 改了 a 结果 b 也变了:引用共享问题。需要独立副本用切片或 deepcopy。
  5. remove 报 ValueError:要删的值不在列表里。先 if v in lst: 再删,或用 discard 语义的 try/except(第 11 课)。
  6. 遍历中增删列表行为诡异:Python 遍历按下标推进,删除会跳元素。改为遍历副本或用推导式生成新列表。

面试常问

  • list 和 tuple 的区别,各自使用场景? list 可变、tuple 不可变。tuple 用于结构固定的数据(坐标、函数多返回值)、作为 dict 键、以及向调用方表达「这份数据不该被改」的语义。不可变还让 tuple 可 hash、更省内存。

  • dict 的底层实现和查找复杂度? 哈希表。键经 hash 定位桶,平均 O(1) 查找/插入,最坏 O(n)(哈希冲突严重时)。这也是「键必须可 hash(不可变)」的原因。Python 3.7+ dict 保序且做了内存紧凑化。

  • set 和 dict 的关系? set 本质是「只有键没有值」的 dict,同样基于哈希表,成员判断平均 O(1)。所以 set 元素也必须不可变、无序、自动去重。

  • 如何对列表去重且保持顺序? list(dict.fromkeys(lst))(3.7+ dict 有序);或遍历 + seen 集合判断。直接 list(set(lst)) 会丢失原顺序。

  • 浅拷贝与深拷贝的区别? 浅拷贝只复制容器本身,内部嵌套对象仍共享引用(lst[:]、list(lst)、copy.copy);深拷贝递归复制所有层级(copy.deepcopy),完全独立。嵌套结构需要真正隔离时必须深拷贝。

自测三问

  1. sorted(lst) 和 lst.sort() 的三个区别(返回值、原列表、调用方式)?
  2. 如何安全地给字典里可能不存在的键累加 1?写出代码。
  3. [1, 2, 2, 3, 3, 3] 去重并保持原顺序,写出代码。

小结一句话

list 排队、dict 通讯录、set 点名册、tuple 石碑;记住 sort 返回 None、get 不炸、含头不含尾、赋值是共享不是复制,容器这一关就通了。

延伸

  • 官方教程 — 数据结构
  • collections 模块(defaultdict、Counter 等)
  • 下一课预告:字符串处理——切片、f-string、split/join,文本是脚本世界处理量最大的数据。
加载本节练习…
← 上一课函数下一课 →字符串处理