Python字典与集合:键值存储、去重与快速查找

假设你在图书馆找一本书。如果不按分类、书名、作者来检索,而是从第一排书架的第一本书开始逐本翻——这个图书馆有十万本书——等你找到想看的书时,图书馆可能已经关门了。

列表就像逐本翻阅:你要找某个元素,最坏情况下需要遍历全部。而字典就像图书馆的检索系统:你给出书名,系统立刻告诉你"它在第 3 排第 2 层"。不管图书馆有一万本还是一百万本书,查找时间几乎不变。

这就是字典的力量。它背后的数据结构叫哈希表,是计算机科学里最重要的发明之一。这篇把字典和它的近亲集合彻底讲清楚。

字典是什么

字典是一组键值对的集合。每个键对应一个值,通过键来查找值——而不是通过位置。

# 字典用花括号 {}
student = {
    "name": "小明",
    "age": 20,
    "score": 85,
    "is_passed": True
}

# 通过键来访问值
print(student["name"])       # "小明"
print(student["score"])      # 85

用真实的例子来感受字典和列表的差别:

# 列表:按位置存储,要知道"第几个"
students_list = ["小明", "小红", "小刚"]
print(students_list[0])   # 小明——你得记住位置

# 字典:按键存储,用有意义的名字
students_dict = {"001": "小明", "002": "小红", "003": "小刚"}
print(students_dict["001"])   # 小明——用学号直接查,语义清晰

列表是"第一个、第二个、第三个"。字典是"名字叫XXX的那个"。大多数情况下,字典的表达方式更符合人的思维方式——我们找人时说的是"找小明",不是"找第 3 个人"。

创建字典

# 直接写
d1 = {"a": 1, "b": 2}

# dict() 构造函数
d2 = dict(name="小明", age=20)           # 关键字参数
d3 = dict([("a", 1), ("b", 2)])          # 从键值对列表
d4 = dict(zip(["a", "b"], [1, 2]))       # 从两个列表配对
d5 = {k: k ** 2 for k in range(5)}       # 字典推导式

# 空字典
d6 = {}

字典的核心操作

student = {"name": "小明", "age": 20}

# 访问
print(student["name"])           # 键不存在时报 KeyError
print(student.get("score"))      # 键不存在时返回 None(不报错)
print(student.get("score", 0))   # 键不存在时返回默认值 0

# 添加或修改
student["score"] = 85             # 键不存在就添加,存在就修改
student["age"] = 21               # 修改已有键的值

# 删除
del student["age"]                # 删除键值对
value = student.pop("score")      # 删除并返回被删的值
value = student.pop("score", 0)   # 键不存在时返回默认值 0

# 检查键是否存在
if "name" in student:
    print("有名字字段")

get() 是处理"可能不存在的键"的最佳方式。新手常写 if key in dict: value = dict[key],但实际上 value = dict.get(key, default) 一行搞定。

遍历字典

student = {"name": "小明", "age": 20, "score": 85}

# 遍历键(默认)
for key in student:
    print(key, student[key])

# 遍历值
for value in student.values():
    print(value)

# 遍历键值对(最常用)
for key, value in student.items():
    print(f"{key} = {value}")

在这里插入图片描述

图8-1 字典哈希表结构:键通过哈希函数映射到存储位置,查找时间接近常数,与字典大小几乎无关。

字典的键为什么必须是不可变的

字典的键必须是**不可变的(可哈希的)**类型:字符串、数字、元组可以作为键;列表、字典、集合不能。

# 合法的键
d = {
    "name": "小明",      # 字符串
    42: "答案",          # 数字
    (116.4, 39.9): "北京"  # 元组
}

# 不合法的键
# d = {[1, 2]: "value"}  # TypeError: unhashable type: 'list'

原因在于字典的底层实现。Python 对键做哈希运算,算出存储位置。如果键是可变的(比如列表),存入后键发生了变化,哈希值也变了,Python 就找不到原来的数据了。不可变类型保证了键的哈希值永远不会变——这是字典高效查找的根本保障。

从使用角度你只需要记住:能用字符串当键就优先用字符串——最直观、最不容易出错、最利于 JSON 序列化。

collections 里的字典增强工具

defaultdict:不用手动初始化

普通字典在不存在的键上累加时会报错:

# 普通字典计数——很啰嗦
text = "hello world"
char_count = {}
for char in text:
    if char not in char_count:
        char_count[char] = 0
    char_count[char] += 1

defaultdict 让你指定默认值类型,不存在的键自动用默认值:

from collections import defaultdict

char_count = defaultdict(int)    # 默认值 0
for char in "hello world":
    if char != " ":
        char_count[char] += 1

print(dict(char_count))  # {'h': 1, 'e': 1, 'l': 3, 'o': 2, 'w': 1, 'r': 1, 'd': 1}

defaultdict(list) 是最常用的模式之一——分组数据时不用判断键是否存在:

# 把学生按班级分组
students = [
    ("小明", "1班"), ("小红", "2班"), ("小刚", "1班"), ("小美", "2班")
]

by_class = defaultdict(list)
for name, cls in students:
    by_class[cls].append(name)

print(dict(by_class))
# {'1班': ['小明', '小刚'], '2班': ['小红', '小美']}

Counter:专门用于计数的字典

from collections import Counter

# 统计单词频率
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_count = Counter(words)
print(word_count)                     # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(word_count.most_common(2))      # [('apple', 3), ('banana', 2)]  最常见的 2 个

# Counter 可以直接相加
c1 = Counter(["a", "b", "a"])
c2 = Counter(["a", "c"])
print(c1 + c2)   # Counter({'a': 3, 'b': 1, 'c': 1})

手写一个词频统计需要七八行代码,Counter 一行搞定。处理日志分析、投票统计、文本处理时它是第一选择。

字典推导式

# 从两个列表构建字典
keys = ["name", "age", "score"]
values = ["小明", 20, 85]
student = {k: v for k, v in zip(keys, values)}
print(student)   # {'name': '小明', 'age': 20, 'score': 85}

# 交换键和值
original = {"a": 1, "b": 2, "c": 3}
swapped = {v: k for k, v in original.items()}
print(swapped)   # {1: 'a', 2: 'b', 3: 'c'}

# 条件过滤
scores = {"小明": 85, "小红": 92, "小刚": 78, "小美": 95}
top_students = {name: score for name, score in scores.items() if score >= 90}
print(top_students)   # {'小红': 92, '小美': 95}

集合:无重复、无序的数据容器

集合是一种特殊的数据结构——它只关心"有没有",不关心"第几个"或"出现了几次"。集合里的元素是唯一的、无序的

# 创建集合
fruits = {"苹果", "香蕉", "橙子"}      # 花括号,但没有键值对
chars = set("hello")                   # {'h', 'e', 'l', 'o'}  自动去重
nums = set([1, 2, 3, 2, 1, 3])        # {1, 2, 3}  自动去重

# 核心操作
fruits = {"苹果", "香蕉", "橙子"}
fruits.add("葡萄")            # 添加
fruits.remove("香蕉")         # 删除(不存在会报错)
fruits.discard("西瓜")        # 删除(不存在也不报错)

print("苹果" in fruits)       # True  快速成员检查
print(len(fruits))            # 集合的大小

集合运算——数学里的交集并集差集

这是集合最强大的功能。你不需要写嵌套循环去对比两个列表——用集合运算一行搞定:

python_students = {"小明", "小红", "小刚", "小美"}
java_students = {"小刚", "小美", "小李", "小王"}

# 交集:两门都报了的人
both = python_students & java_students      # {'小刚', '小美'}
both = python_students.intersection(java_students)

# 并集:所有报过课的人
all_students = python_students | java_students
# {'小明', '小红', '小刚', '小美', '小李', '小王'}

# 差集:只报了 Python 没报 Java 的人
only_python = python_students - java_students   # {'小明', '小红'}

# 对称差集:只报了一门的人
only_one = python_students ^ java_students     # {'小明', '小红', '小李', '小王'}

在这里插入图片描述

图8-2 集合运算:交集(&)、并集(|)、差集(-)、对称差集(^)四种运算的韦恩图对比。

列表去重的最快方式

items = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(items))
print(unique)   # [1, 2, 3, 4]  顺序可能不保持!

注意:set() 不保证顺序。如果你需要去重且保持原顺序,用 dict.fromkeys()

items = [3, 1, 2, 1, 3, 2, 4]
unique_ordered = list(dict.fromkeys(items))
print(unique_ordered)  # [3, 1, 2, 4]  保持第一次出现的顺序

集合运算的实际应用——比你想的更常见

集合的交并差运算不是数学课上的抽象概念——它们在日常编程中比你想象中更频繁地出现。社交网络中的"共同好友"功能本质上就是两个集合的交集。推荐系统中的"猜你喜欢"有时候就是"与你相似的人喜欢了什么"——集合的差集。黑名单过滤就是"发送名单减去黑名单"。

用列表来实现同样的逻辑需要嵌套循环——外层遍历一个列表,内层检查是否在另一个列表中。时间复杂度 O(N×M)。用集合的交并差,Python 在底层用哈希表实现,时间复杂度接近 O(N+M)。数据量越大,这个差异越明显。这就是选择正确数据结构带来的实实在在的性能红利。

任何时候你发现自己在写"遍历 A,检查是否在 B 中"的模式,停下来想一想——是不是应该把 B 转成集合?在 1000 条数据里,性能差距可能是 1000 倍。养成这个习惯,你的代码从"能跑"升级到"跑得快"。

集合操作实战——两个实际场景的完整代码

场景一:找出两个班级报相同社团的学生

class_a_clubs = {"小明": {"编程", "篮球"}, "小红": {"编程", "舞蹈"}, "小刚": {"篮球", "音乐"}}
class_b_clubs = {"小李": {"编程", "足球"}, "小王": {"舞蹈", "音乐"}, "小赵": {"编程", "篮球"}}

# 找出所有社团(并集)
all_clubs = set()
for clubs in class_a_clubs.values():
    all_clubs |= clubs
for clubs in class_b_clubs.values():
    all_clubs |= clubs
print(f"所有社团:{all_clubs}")

# 找出两个班都有的社团(交集)
clubs_a = set().union(*class_a_clubs.values())
clubs_b = set().union(*class_b_clubs.values())
common_clubs = clubs_a & clubs_b
print(f"两个班都有的社团:{common_clubs}")

场景二:用集合去重并保持顺序

def deduplicate_keep_order(items):
    """去重但保持第一次出现的顺序"""
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(deduplicate_keep_order(data))  # [3, 1, 4, 5, 9, 2, 6]

这个模式在数据清洗中非常常见——你需要去重,但顺序对后续处理很重要。Python 3.7+ 也可以用 list(dict.fromkeys(data)) 一行搞定,因为字典保持了插入顺序。

这个选型决策每天都会遇到。一个简单的决策框架:

需求用什么为什么
按位置访问,顺序重要列表列表是有序的,索引天然表达位置
按键名查找,键有意义字典字典通过键查找,时间复杂度 O(1)
只要不重复,不关心顺序集合自动去重 + 快速成员检查
需要统计频次Counter专门为计数优化的字典
需要分组defaultdict(list)省去手动初始化的代码
需要一个简单的记录结构字典 / namedtuplenamedtuple 更省内存,字典更灵活

日常编码中最常见的搭配是:列表存"一组相似的数据",字典存"需要按键查找的记录",集合在做"去重"和"交并差"运算时登场

实战:用字典和集合解决实际问题

程序一:学生信息管理

def student_manager():
    """用字典管理学生信息,实现查找和统计"""
    students = {
        "001": {"name": "小明", "age": 20, "scores": [85, 92, 78]},
        "002": {"name": "小红", "age": 21, "scores": [92, 95, 88]},
        "003": {"name": "小刚", "age": 20, "scores": [78, 82, 80]},
    }
    
    # 查找学生
    sid = input("输入学号:").strip()
    student = students.get(sid)
    if student is None:
        print("学号不存在")
    else:
        avg = sum(student["scores"]) / len(student["scores"])
        print(f"{student['name']}{student['age']}岁,平均分 {avg:.1f}")
    
    # 统计各班人数
    by_age = defaultdict(list)
    for sid, info in students.items():
        by_age[info["age"]].append(info["name"])
    
    for age, names in sorted(by_age.items()):
        print(f"{age}岁:{', '.join(names)}")

# student_manager()

程序二:共同好友查找

def find_mutual_friends():
    """找出两个人的共同好友"""
    alice_friends = {"Bob", "Charlie", "David", "Eve"}
    bob_friends = {"Charlie", "Eve", "Frank", "Grace"}
    
    mutual = alice_friends & bob_friends
    only_alice = alice_friends - bob_friends
    only_bob = bob_friends - alice_friends
    all_friends = alice_friends | bob_friends
    
    print(f"共同好友:{mutual}")
    print(f"Alice 独有:{only_alice}")
    print(f"Bob 独有:{only_bob}")
    print(f"所有好友:{all_friends}")
    print(f"好友总数:{len(all_friends)}")

# find_mutual_friends()

程序三:词频统计器

from collections import Counter
import re

def word_frequency(text):
    """统计英文文本中的词频"""
    words = re.findall(r'\b\w+\b', text.lower())
    freq = Counter(words)
    
    print("最常见的 5 个单词:")
    for word, count in freq.most_common(5):
        print(f"  {word}: {count} 次")
    
    return freq

sample = """
Python is a programming language. Python is easy to learn.
It is widely used in data science and web development.
"""
word_frequency(sample)

字典在 Python 3.7+ 中是有序的

从 Python 3.7 开始,字典保证插入顺序——你按什么顺序添加键,遍历时就按什么顺序出来。这在实际编码中非常有用——你不再需要 collections.OrderedDict 来保持顺序。

d = {}
d["c"] = 3
d["a"] = 1
d["b"] = 2
print(list(d.keys()))   # ['c', 'a', 'b']  按插入顺序

不过要注意:不是说字典"像列表一样有序"——你不能通过索引来访问字典。它只是保证迭代顺序和插入顺序一致。这个特性是 CPython 3.6 的实现细节,3.7 起成为语言规范。

集合的性能优势

集合和字典一样基于哈希表,所以它的成员检查速度也是 O(1)。当你需要反复检查"某个元素在不在"时,集合远比列表高效:

# 在 10 万个数据中检查 1000 个目标
big_list = list(range(100000))
big_set = set(range(100000))
targets = range(0, 1000)

# 用列表:每次都要遍历查找,慢
# for t in targets:
#     if t in big_list:
#         pass

# 用集合:每次查找都是 O(1),快得多
# for t in targets:
#     if t in big_set:
#         pass

当你的程序在处理成百上千次成员检查时,把列表转换成集合能带来数量级的性能提升。

在这里插入图片描述

图8-3 字典与集合应用场景:字典适合键值查找和结构化记录,集合适合去重和集合运算。

从零实现一个"词频统计器"——感受字典的真正力量

学了一堆字典操作,不如写一个真正有用的程序。搜索引擎、推荐系统、文本分析的底层逻辑,都依赖于"统计每个词出现了多少次":

from collections import Counter
import re

def analyze_text(text):
    words = re.findall(r'\b\w+\b', text.lower())
    freq = Counter(words)
    print(f"总词数:{len(words)},不重复词数:{len(freq)}")
    print(f"\n最常见的 10 个单词:")
    for word, count in freq.most_common(10):
        print(f"  {word:<15} {'█' * min(count, 30)} {count}")
    return freq

sample = "Python is powerful and fast. Python is easy to learn. Python everywhere."
analyze_text(sample)

运行后你会看到 “python” 出现了 3 次、“is” 出现了 2 次……这个简单的统计背后,Counter 内部维护了一个键为单词、值为计数的字典。没有 Counter 之前,你需要写 if word in freq: freq[word] += 1 else: freq[word] = 1,现在一行搞定。选择正确的数据结构,代码就是你意图的直译而不是充满了索引和循环细节。

动手练习

  1. 字典合并实验:用三种方式合并两个字典(update(){**d1, **d2}d1 | d2(Python 3.9+)),分别计时 100 万次合并操作,比较三种方式的性能差异。然后用 sys.getsizeof() 比较合并前后的内存占用。

  2. 字母频次统计:读取一段中文或英文文本,用 Counter 统计每个字符(或字母)的出现次数,输出前 10 个最高频的。

  3. 选课冲突检测:给两个学生各自选了哪些课(用集合表示),找出他们选了相同的课(交集)、只有 A 学生选的课(差集)、两人选的所有课(并集)。

  4. 电话号码簿:用字典实现一个电话簿,支持添加联系人、按名字查找号码、按号码查找名字、删除联系人。用 get() 优雅地处理不存在的联系人。

  5. 用 defaultdict 分组:有一组商品数据,每个商品包含名称和分类。用 defaultdict(list) 按分类分组商品,输出每个分类下的商品列表。

  6. 集合去重性能对比:创建一个包含 10 万个元素的列表(用 list(range(50000)) * 2 生成大量重复),分别用"循环 + 列表"和 set() 两种方式去重。用 time.perf_counter() 计时,比较两者速度差距。你会直观感受到为什么能选集合就不要手写循环。

字典的"瑞士军刀"——get、setdefault、update 和字典合并

除了 get(),字典还有几个不常用但威力巨大的方法。掌握它们能让很多常见操作从四五行代码变成一行:

# setdefault:如果键不存在,设置并返回默认值;如果存在,返回现有值
word_count = {}
for word in "hello world hello python".split():
    word_count.setdefault(word, 0)
    word_count[word] += 1

# update:批量更新字典(覆盖已有键,添加新键)
config = {"host": "localhost", "port": 8080}
config.update({"port": 9090, "debug": True})  # port 被覆盖,debug 被添加
print(config)  # {'host': 'localhost', 'port': 9090, 'debug': True}

# Python 3.9+ 字典合并运算符
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
merged = d1 | d2          # {'a': 1, 'b': 3, 'c': 4} ——后者覆盖前者
d1 |= d2                  # d1 现在被合并了(原地修改)

setdefault 在处理嵌套字典时尤其强大。比如你要给一个不存在的键初始化一个列表——data.setdefault("students", []).append("小明"),一行代码完成了"如果键不存在则创建空列表,然后添加元素"的完整逻辑。相比传统的 if key not in data: data[key] = []; data[key].append(item),简洁度和可读性都提升了不止一个档次。


字典和列表的性能差异——为什么"用空间换时间"是值得的

列表查找一个元素需要从头到尾逐个比对,最坏情况下检查完所有元素才能确认"不存在"。如果你有 10 万条数据,用列表做成员检查可能要做 10 万次对比。而字典和集合在查找时只做一次哈希计算和一次索引查找,几乎和数据量无关——1 条数据和 100 万条数据,"在不在"这个问题的回答速度几乎一样快。

字典的这个能力不是免费的——它用更多的内存来换取查找速度。这就是经典的计算机科学权衡:空间换时间。你的手机通讯录用的是类似字典的结构——输入"妈妈"立刻跳出号码,而不是从第一个联系人逐个翻找。

在日常编程中,这个差异意味着:当你需要频繁查找(“这个用户是否在黑名单里”“这个商品编号是否已存在”),第一时间应该想到集合或字典。用列表来干同样的事,在小数据量时没有问题,但数据量一上去就会成为性能瓶颈。养成用正确数据结构的好习惯,比后期优化代码要省力得多。实际上,许多程序性能问题的根源都不是"算法不够优化",而是"用错了数据结构"——把应该用字典的查找任务交给了列表,把应该用集合的去重任务交给了嵌套循环。花一点时间在选对数据结构上,比你后期花大量时间在优化算法上要高效得多。数据结构选对了,代码几乎自然就是高效的。

现实中的字典应用——三个你每天在用的例子

你可能意识不到,但你每天都在使用基于字典数据结构的服务。微信的通讯录——名字到微信号的映射,就是字典。浏览器的缓存——URL 到网页内容的映射,也是字典。数据库的索引——字段值到存储位置的映射,本质上还是字典的各种变种。

在 Python 编程中,字典最常见的三个应用场景是:数据聚合(按某个字段分组统计)、缓存(存下计算结果避免重复计算)、配置管理(键值对形式的应用参数)。学到第 12 篇处理 JSON 数据、第 18 篇使用第三方 API 时,你会反复用到字典。它是你编程工具箱里使用频率仅次于列表的数据结构。


本篇要点回顾

  • 字典是键值对集合,用键来查找值 O(1),适合"按名字找数据"的场景
  • 字典的键必须是不可变类型——字符串、数字、元组;列表不能做键
  • dict.get(key, default)if key in dict 更优雅
  • defaultdict 省去手动初始化,Counter 是计数的瑞士军刀
  • 集合是无重复元素的容器,成员检查和去重极快
  • 集合支持数学运算:交集 &、并集 |、差集 -、对称差 ^
  • Python 3.7+ 字典保持插入顺序

下一篇我们攻克字符串处理——切片进阶、格式化全解、编码问题、正则表达式入门,以及二十几个你必须掌握的字符串方法。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐