上一篇文章我们学习了列表和元组这两种有序的序列类型。今天我们要学习的字典集合则是以哈希表为基础的另一类数据结构,它们的特点是无序、高效,在实际编程中有着极其广泛的应用。

字典(Dict)

字典是一种键值对映射结构,通过键来快速查找对应的值。你可以把它想象成一本真正的字典——通过"词条"(键)来查找"释义"(值)。

创建字典

# 多种创建字典的方式

# 方式1:花括号
student = {
    "name": "张三",
    "age": 20,
    "score": 85
}
print(student)  # {'name': '张三', 'age': 20, 'score': 85}

# 方式2:dict() 构造函数
person = dict(name="李四", age=25, city="北京")
print(person)  # {'name': '李四', 'age': 25, 'city': '北京'}

# 方式3:由键值对列表创建
pairs = [("name", "王五"), ("age", 30), ("city", "上海")]
data = dict(pairs)
print(data)  # {'name': '王五', 'age': 30, 'city': '上海'}

# 方式4:zip 创建
keys = ["name", "age", "city"]
values = ["赵六", 22, "广州"]
info = dict(zip(keys, values))
print(info)  # {'name': '赵六', 'age': 22, 'city': '广州'}

# 方式5:字典推导式
squares = {x: x ** 2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 空字典
empty_dict = {}
empty_dict2 = dict()

访问字典元素

student = {"name": "张三", "age": 20, "score": 85}

# 使用方括号访问(键不存在会报错)
print(student["name"])    # 张三
print(student["age"])     # 20
# print(student["grade"]) # KeyError!

# 使用 get() 安全访问(键不存在返回 None 或默认值)
print(student.get("name"))         # 张三
print(student.get("grade"))        # None
print(student.get("grade", "N/A")) # N/A(指定默认值)

# 获取所有键、值、键值对
print(student.keys())     # dict_keys(['name', 'age', 'score'])
print(student.values())   # dict_values(['张三', 20, 85])
print(student.items())    # dict_items([('name', '张三'), ('age', 20), ('score', 85)])

# 成员检查(默认检查键)
print("name" in student)    # True
print("张三" in student)   # False(这里是检查键,不是值)
print(85 in student.values())  # True(检查值)

修改字典

student = {"name": "张三", "age": 20, "score": 85}

# 修改已有键的值
student["age"] = 21
print(student)  # {'name': '张三', 'age': 21, 'score': 85}

# 添加新键值对
student["grade"] = "大三"
print(student)  # {'name': '张三', 'age': 21, 'score': 85, 'grade': '大三'}

# update() - 合并另一个字典
new_info = {"city": "北京", "age": 22}
student.update(new_info)
print(student)  # {'name': '张三', 'age': 22, 'score': 85, 'grade': '大三', 'city': '北京'}

# update() 也可以传入键值对
student.update(hobby="编程", gender="男")
print(student)
# {'name': '张三', 'age': 22, 'score': 85, 'grade': '大三', 'city': '北京', 'hobby': '编程', 'gender': '男'}

删除字典元素

student = {"name": "张三", "age": 20, "score": 85, "grade": "大三", "city": "北京"}

# pop() - 删除指定键并返回值
age = student.pop("age")
print(age)      # 20
print(student)  # {'name': '张三', 'score': 85, 'grade': '大三', 'city': '北京'}

# pop() 可以指定默认值,防止 KeyError
grade = student.pop("grade", "未知")
print(grade)    # 大三(存在则返回)

grade = student.pop("grade", "未知")  # 第二次删
print(grade)    # 未知(不存在返回默认值)

# popitem() - 删除并返回最后一个键值对(Python 3.7+ 有序)
item = student.popitem()
print(item)     # ('city', '北京')(最后一个)
print(student)  # {'name': '张三', 'score': 85}

# del - 删除指定键
del student["score"]
print(student)  # {'name': '张三'}

# clear() - 清空字典
student.clear()
print(student)  # {}

字典方法总结

方法 说明
d.get(key, default) 获取键的值,不存在返回 default
d.keys() 返回所有键的视图
d.values() 返回所有值的视图
d.items() 返回所有 (键, 值) 对的视图
d.update(d2) 合并另一个字典
d.pop(key, default) 删除键并返回值
d.popitem() 删除并返回最后插入的键值对
d.setdefault(key, default) 键存在返回值,不存在则设置默认值
d.clear() 清空字典
d.copy() 复制字典
d.fromkeys(seq, value) 从序列创建字典,统一设置值

setdefault() 的妙用

# setdefault() 是个很有用的方法
# 如果键存在,返回其值;如果不存在,设置默认值并返回

# 统计单词出现次数(传统方式)
text = "apple banana apple orange banana apple"
words = text.split()
count = {}
for word in words:
    if word not in count:
        count[word] = 0
    count[word] += 1
print(count)  # {'apple': 3, 'banana': 2, 'orange': 1}

# setdefault() 简化版本
count = {}
for word in words:
    count.setdefault(word, 0)
    count[word] += 1
print(count)  # {'apple': 3, 'banana': 2, 'orange': 1}

# 更简洁的方式:使用 collections.defaultdict
from collections import defaultdict
count = defaultdict(int)
for word in words:
    count[word] += 1
print(dict(count))  # {'apple': 3, 'banana': 2, 'orange': 1}

# 在嵌套字典中的用法
data = {}
data.setdefault("users", {})
data["users"].setdefault("张三", {})
data["users"]["张三"]["age"] = 20
print(data)  # {'users': {'张三': {'age': 20}}}

字典推导式

# 基本形式:{key_expr: value_expr for item in iterable}
squares = {x: x ** 2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 带条件筛选
even_squares = {x: x ** 2 for x in range(10) if x % 2 == 0}
print(even_squares)  # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

# 使用已有字典创建新字典
original = {"a": 1, "b": 2, "c": 3}
doubled = {k: v * 2 for k, v in original.items()}
print(doubled)  # {'a': 2, 'b': 4, 'c': 6}

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

# 转换键名
str_nums = {"1": "one", "2": "two", "3": "three"}
int_nums = {int(k): v for k, v in str_nums.items()}
print(int_nums)  # {1: 'one', 2: 'two', 3: 'three'}

# 过滤字典
scores = {"张三": 85, "李四": 92, "王五": 78, "赵六": 95}
passed = {name: score for name, score in scores.items() if score >= 80}
print(passed)  # {'张三': 85, '李四': 92, '赵六': 95}

嵌套字典

在实际开发中,嵌套字典非常常见,例如表示 JSON 数据:

# 嵌套字典示例:表示学生信息
students = {
    "001": {
        "name": "张三",
        "age": 20,
        "courses": {
            "Python": 85,
            "数学": 92,
            "英语": 78
        }
    },
    "002": {
        "name": "李四",
        "age": 22,
        "courses": {
            "Python": 95,
            "数学": 88,
            "英语": 90
        }
    }
}

# 访问嵌套数据
print(students["001"]["name"])            # 张三
print(students["001"]["courses"]["Python"]) # 85

# 使用 get() 安全访问嵌套数据
print(students.get("003", {}).get("name", "未知"))  # 未知

# 遍历嵌套字典
for student_id, info in students.items():
    print(f"\n学号: {student_id}")
    print(f"姓名: {info['name']}")
    avg_score = sum(info['courses'].values()) / len(info['courses'])
    print(f"平均分: {avg_score:.1f}")

集合(Set)

集合是一个无序的、不重复的元素集合。它主要用于去重和集合运算(交集、并集、差集等)。

创建集合

# 创建集合的几种方式

# 方式1:花括号
fruits = {"苹果", "香蕉", "橙子", "苹果"}  # 重复的"苹果"会被自动去重
print(fruits)  # {'香蕉', '橙子', '苹果'}(顺序可能不同)

# 方式2:set() 构造函数
numbers = set([1, 2, 2, 3, 3, 3])  # 从列表创建
print(numbers)  # {1, 2, 3}(自动去重)

# 方式3:从字符串创建
chars = set("hello")
print(chars)  # {'h', 'e', 'l', 'o'}(l 只出现一次)

# 方式4:集合推导式
evens = {x for x in range(10) if x % 2 == 0}
print(evens)  # {0, 2, 4, 6, 8}

# 注意:空集合必须用 set(),不能用 {}
empty_set = set()     # 空集合
not_empty = {}        # 这是空字典!
print(type(empty_set))   # <class 'set'>
print(type(not_empty))   # <class 'dict'>

集合操作

集合支持丰富的数学运算——交集、并集、差集、对称差:

a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7, 8}

# 并集:所有元素
print(a | b)        # {1, 2, 3, 4, 5, 6, 7, 8}
print(a.union(b))   # 同上

# 交集:共有的元素
print(a & b)        # {4, 5}
print(a.intersection(b))  # 同上

# 差集:在 a 中但不在 b 中的元素
print(a - b)        # {1, 2, 3}
print(a.difference(b))  # 同上

print(b - a)        # {6, 7, 8}

# 对称差:除了交集以外的所有元素
print(a ^ b)        # {1, 2, 3, 6, 7, 8}
print(a.symmetric_difference(b))  # 同上

集合的修改

fruits = {"苹果", "香蕉"}

# 添加元素
fruits.add("橙子")
print(fruits)  # {'苹果', '香蕉', '橙子'}

# 删除元素
fruits.remove("香蕉")      # 不存在时会报 KeyError
print(fruits)              # {'苹果', '橙子'}

# discard() 安全删除(不存在不会报错)
fruits.discard("葡萄")     # 没有"葡萄",但不会报错

# pop() 随机删除并返回一个元素
popped = fruits.pop()
print(popped)  # 随机返回一个

# clear() 清空集合
fruits.clear()
print(fruits)  # set()

# update() 批量添加
numbers = {1, 2, 3}
numbers.update([3, 4, 5])
print(numbers)  # {1, 2, 3, 4, 5}

集合的成员检查和子集判断

# 极快的成员检查(O(1))
large_set = set(range(100000))
print(99999 in large_set)  # True(非常快)
print(200000 in large_set) # False

# 子集/超集判断
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
c = {1, 2, 3}

print(a.issubset(b))     # True(a 是 b 的子集)
print(b.issuperset(a))   # True(b 是 a 的超集)
print(a.issubset(c))     # True(集合是自己的子集)
print(a == c)            # True(相等)
print(a.isdisjoint({4, 5}))  # True(没有交集)

集合推导式

# 基本形式
squares = {x ** 2 for x in range(10)}
print(squares)  # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

# 带条件
even_squares = {x ** 2 for x in range(20) if x % 2 == 0}
print(even_squares)  # {0, 4, 16, 36, 64, 100, 144, 196, 256, 324}

# 实用:从文本中提取唯一单词
text = "the cat sat on the mat the cat is happy"
unique_words = {word for word in text.split()}
print(sorted(unique_words))
# ['cat', 'happy', 'is', 'mat', 'on', 'sat', 'the']

frozenset(不可变集合)

frozenset 是集合的不可变版本,可以作为字典键或集合的元素:

# 创建 frozenset
fs = frozenset([1, 2, 3, 3, 4])
print(fs)        # frozenset({1, 2, 3, 4})
print(type(fs))  # <class 'frozenset'>

# frozenset 不可变
# fs.add(5)     # AttributeError!

# 可以作为字典键
teams = {
    frozenset(["张三", "李四"]): "团队A",
    frozenset(["王五", "赵六"]): "团队B"
}
print(teams)  # {frozenset({'张三', '李四'}): '团队A', frozenset({'王五', '赵六'}): '团队B'}

# frozenset 支持所有集合运算
a = frozenset([1, 2, 3])
b = frozenset([2, 3, 4])
print(a & b)  # frozenset({2, 3})

字典 vs 列表的查找性能对比

import time

# 列表查找是 O(n)
n = 100000
list_data = list(range(n))
dict_data = {i: i for i in range(n)}

# 查找列表中的最后一个元素
start = time.perf_counter()
print(99999 in list_data)
list_time = time.perf_counter() - start

# 字典/集合查找是 O(1)
start = time.perf_counter()
print(99999 in dict_data)
dict_time = time.perf_counter() - start

print(f"列表查找: {list_time:.6f}s")
print(f"字典查找: {dict_time:.6f}s")
print(f"字典比列表快约 {list_time/dict_time:.0f} 倍")

实际应用场景

1. 去重

# 使用集合去重
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(numbers))
print(unique)  # [1, 2, 3, 4](注意:顺序可能不同)

# 保持顺序去重
def unique_ordered(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

words = ["apple", "banana", "apple", "orange", "banana"]
print(unique_ordered(words))  # ['apple', 'banana', 'orange']

2. 数据分组

# 使用字典分组数据
students = [
    ("张三", "A班"),
    ("李四", "B班"),
    ("王五", "A班"),
    ("赵六", "B班"),
    ("钱七", "A班"),
]

# 按班级分组
classes = {}
for name, cls in students:
    classes.setdefault(cls, []).append(name)

print(classes)
# {'A班': ['张三', '王五', '钱七'], 'B班': ['李四', '赵六']}

3. 统计频率

text = "the cat in the hat sat on the mat"

# 统计单词频率
word_counts = {}
for word in text.split():
    word_counts[word] = word_counts.get(word, 0) + 1

print(word_counts)
# {'the': 3, 'cat': 1, 'in': 1, 'hat': 1, 'sat': 1, 'on': 1, 'mat': 1}

# 按频率排序
sorted_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)
for word, count in sorted_words:
    print(f"{word}: {count}")

4. 缓存/记忆化

# 用字典做缓存
cache = {}

def fibonacci(n):
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    result = fibonacci(n - 1) + fibonacci(n - 2)
    cache[n] = result
    return result

# 第一次计算
print(fibonacci(35))  # 9227465(递归计算)

# 第二次(直接返回缓存结果)
print(fibonacci(35))  # 9227465(直接从缓存获取)

实践练习:通讯录管理系统

# contacts.py
print("=" * 40)
print("      通讯录管理系统")
print("=" * 40)

# 使用字典存储联系人
contacts = {}

def add_contact(name, phone, email=""):
    """添加联系人"""
    contacts[name] = {"phone": phone, "email": email}
    print(f"已添加联系人: {name}")

def delete_contact(name):
    """删除联系人"""
    if name in contacts:
        del contacts[name]
        print(f"已删除联系人: {name}")
    else:
        print(f"未找到联系人: {name}")

def search_contact(name):
    """查找联系人"""
    if name in contacts:
        info = contacts[name]
        print(f"姓名: {name}")
        print(f"电话: {info['phone']}")
        print(f"邮箱: {info.get('email', '未设置')}")
    else:
        print(f"未找到联系人: {name}")

def list_contacts():
    """列出所有联系人"""
    if not contacts:
        print("通讯录为空")
        return
    print(f"\n共有 {len(contacts)} 位联系人:")
    print("-" * 30)
    for name, info in sorted(contacts.items()):
        print(f"{name:10s} {info['phone']:15s}")

def get_all_phones():
    """获取所有电话号码(集合去重示例)"""
    phones = {info["phone"] for info in contacts.values()}
    return phones

# 测试
add_contact("张三", "138-0001-0001", "zhangsan@email.com")
add_contact("李四", "138-0002-0002")
add_contact("王五", "138-0001-0001")  # 和张三同号
add_contact("赵六", "138-0003-0003", "zhaoliu@email.com")

print("\n--- 搜索联系人与列表 ---")
search_contact("张三")
list_contacts()

print("\n--- 集合去重:所有不重复的电话号码 ---")
all_phones = get_all_phones()
for phone in all_phones:
    print(f"  {phone}")

常见陷阱

1. 字典的键必须是不可变类型

# 正确:字符串、数字、元组等不可变类型可以作为键
valid = {
    "name": "张三",   # 字符串
    1: "数字",         # 整数
    (1, 2): "元组",   # 元组
}

# 错误:列表、字典等可变类型不能作为键
# invalid = {
#     [1, 2]: "列表",  # TypeError: unhashable type: 'list'
# }

2. 集合不支持索引

# 错误:集合是无序的,不支持索引
# my_set = {1, 2, 3}
# print(my_set[0])  # TypeError!

# 正确:转换为列表
# print(list(my_set)[0])

3. 空花括号是字典不是集合

empty = {}
print(type(empty))  # <class 'dict'>

# 创建空集合必须用 set()
empty_set = set()
print(type(empty_set))  # <class 'set'>

4. 字典的键顺序

# Python 3.7+ 字典保持插入顺序
d = {}
d["b"] = 2
d["a"] = 1
d["c"] = 3
print(list(d.keys()))  # ['b', 'a', 'c'](插入顺序)

# Python 3.6 及以下版本不保证顺序

小结

在这篇文章中,我们学习了:

  1. 字典的创建、访问、修改和常用方法
  2. 字典推导式和嵌套字典
  3. 使用 setdefault()defaultdict 简化代码
  4. 集合的创建、集合运算(交、并、差、对称差)
  5. 集合推导式frozenset
  6. 字典与集合的性能优势
  7. 实际应用:去重、分组、统计频率、缓存

下一步

现在你已经掌握了 Python 最常用的四种数据结构:列表、元组、字典和集合。下一篇文章我们将学习条件判断与分支控制,让程序能够根据不同条件执行不同的逻辑。

实践建议:创建一个简单的英汉字典程序,支持添加单词、查询释义、列出所有单词等功能。练习使用集合去除列表中的重复元素。

Summary: 字典与集合的操作方法详解。