16. 字典:将键映射到值
在前面的章节中,我们学习了列表(list)和元组(tuple)——它们是按特定顺序存储元素的集合,并且允许我们按位置访问它们。但如果我们想用比数字更有意义的东西来查找信息呢?如果我们想通过学生的名字找到他们的成绩,或者通过产品的 ID 找到它的价格,或者通过单词本身找到它的定义呢?
这就是字典(dictionary)派上用场的地方。字典是 Python 内置的数据结构,用于存储键值对(key-value pairs)。我们不再按位置访问元素(例如 grades[0]),而是按它们的键(key)访问(例如 grades["Alice"])。这让字典在真实世界的程序中用于组织和检索数据时变得非常强大。
把字典想象成现实中的词典或电话簿:你查一个单词(键)来找到它的释义(值),或者你查一个名字来找到电话号码。Python 字典的工作方式也是一样——它们将键映射到值,从而实现快速查找和灵活的数据组织。
16.1) 创建字典并访问值
16.1.1) 什么是字典?
字典(dictionary)是由键值对(key-value pairs)组成的集合。每个键都关联着一个值,你使用键来检索值。键在字典中必须是唯一的——你不能有两个条目使用相同的键。不过,值可以重复。
它的基本结构是:
- 键(Keys):用于查找值的唯一标识符(如姓名、ID 或标签)
- 值(Values):与每个键关联的数据(如成绩、价格或描述)
字典具有:
- 可变(mutable):创建后你可以添加、修改或移除键值对
- 无序(unordered)(在 Python 3.6 及更早版本)或插入有序(insertion-ordered)(在 Python 3.7+):虽然现代 Python 会保留你添加条目的顺序,但你应该把字典视为通过键访问条目的集合,而不是通过位置访问
- 动态(dynamic):它们可以按需要增长或缩小
16.1.2) 创建空字典和简单字典
创建字典最简单的方式是使用花括号 {},并用冒号分隔键值对:
# 空字典
empty_dict = {}
print(empty_dict) # Output: {}
print(type(empty_dict)) # Output: <class 'dict'>
# 包含学生成绩的字典
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
# 包含产品价格的字典
prices = {"apple": 0.50, "banana": 0.30, "orange": 0.75}
print(prices) # Output: {'apple': 0.5, 'banana': 0.3, 'orange': 0.75}注意语法:每个键值对写作 key: value,并用逗号分隔。这里的键是字符串("Alice"、"apple"),值是数字,但键和值都可以是许多不同的类型。
你也可以使用 dict() 构造器来创建字典:
# 使用 dict() + 关键字参数
student = dict(name="Alice", age=20, major="Computer Science")
print(student) # Output: {'name': 'Alice', 'age': 20, 'major': 'Computer Science'}
# 使用 dict() + 元组列表
colors = dict([("red", "#FF0000"), ("green", "#00FF00"), ("blue", "#0000FF")])
print(colors) # Output: {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'}当你从其他数据结构构建字典,或想使用 Python 标识符作为键(无需加引号)时,dict() 构造器会很有用。
16.1.3) 通过键访问值
要从字典中取出一个值,可以使用方括号并提供键:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 访问单个值
alice_grade = grades["Alice"]
print(alice_grade) # Output: 95
bob_grade = grades["Bob"]
print(bob_grade) # Output: 87这是访问字典值最直接的方法。但是,如果你尝试访问一个不存在的键,Python 会抛出 KeyError:
grades = {"Alice": 95, "Bob": 87}
# 警告:KeyError - 仅用于演示
# print(grades["David"]) # PROBLEM: KeyError: 'David'这个错误出现的原因是 "David" 不是该字典中的键。我们会在下一小节学习如何安全地处理这种情况。
16.1.4) 使用 get() 安全访问
为了避免在键可能不存在时触发 KeyError,可以使用 get() 方法。如果找不到键,它会返回 None(或你指定的默认值):
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 使用 get() 安全访问
alice_grade = grades.get("Alice")
print(alice_grade) # Output: 95
# 键不存在 - 返回 None
david_grade = grades.get("David")
print(david_grade) # Output: None
# 提供默认值
david_grade = grades.get("David", 0)
print(david_grade) # Output: 0
# 在条件逻辑中使用 get()
if grades.get("Eve") is None:
print("Eve is not in the grade book") # Output: Eve is not in the grade book当你不确定某个键是否存在时,get() 方法比直接用方括号访问更安全。get() 的第二个参数是在键缺失时返回的默认值——如果你不提供,它默认是 None。
下面是一个实际例子,展示 get() 在何时有用:
# 带可选信息的学生数据库
students = {
"Alice": {"age": 20, "major": "CS"},
"Bob": {"age": 19}, # Bob 还没有声明专业
"Charlie": {"major": "Math"} # Charlie 的年龄未记录
}
# 安全访问可能缺失的信息
for name in ["Alice", "Bob", "Charlie"]:
student = students[name]
age = student.get("age", "Unknown")
major = student.get("major", "Undeclared")
print(f"{name}: Age {age}, Major {major}")
# Output:
# Alice: Age 20, Major CS
# Bob: Age 19, Major Undeclared
# Charlie: Age Unknown, Major Math16.1.5) 合法的键类型
字典的键必须是可哈希(hashable)的——这是一个技术术语,意思是键的值不能改变。实践中,这意味着:
合法的键类型(不可变):
- 字符串:
"name"、"id_123" - 数字:
42、3.14 - 元组(且只包含不可变元素):
(1, 2)、("x", "y") - 布尔值:
True、False None
不合法的键类型(可变):
- 列表:
[1, 2, 3]不能作为键 - 字典:
{"a": 1}不能作为键 - 集合:
{1, 2, 3}不能作为键
# 合法的键
valid_dict = {
"name": "Alice", # 字符串键
42: "answer", # 整数键
3.14: "pi", # 浮点数键
(1, 2): "coordinates", # 元组键
True: "yes", # 布尔键
None: "nothing" # None 键
}
print(valid_dict["name"]) # Output: Alice
print(valid_dict[42]) # Output: answer
print(valid_dict[(1, 2)]) # Output: coordinates
# 警告:不合法的键 - 仅用于演示
# invalid_dict = {[1, 2]: "list key"} # PROBLEM: TypeError: unhashable type: 'list'
# invalid_dict = {{1, 2}: "set key"} # PROBLEM: TypeError: unhashable type: 'set'初学者常见错误:一个常见的错误是尝试把列表用作字典键,因为这看起来很合理。例如,你可能想用坐标 [x, y] 作为键来存储位置数据。当 Python 抛出 TypeError: unhashable type: 'list' 时,初学者往往不理解原因——毕竟,这个列表包含的正是他们想作为键使用的数据。
原因在于列表是可变的(它们可以改变),而 Python 需要字典键是稳定且不可改变的。如果你需要用类似列表的东西作为键,先把它转换为元组:tuple([1, 2]) 会变成 (1, 2),这样就可以作为键使用。元组是不可变的,所以它们非常适合:
# 错误:尝试把列表用作键
# locations = {[0, 0]: "origin", [1, 0]: "east"} # PROBLEM: TypeError
# 正确:转换为元组
locations = {(0, 0): "origin", (1, 0): "east", (0, 1): "north"}
print(locations[(0, 0)]) # Output: origin
print(locations[(1, 0)]) # Output: east而值则可以是任何类型——可变或不可变都行:
# 值可以是任何类型
flexible_dict = {
"numbers": [1, 2, 3], # 列表值
"nested": {"a": 1, "b": 2}, # 字典值
"function": len, # 函数值
"mixed": (1, [2, 3], {"x": 4}) # 包含可变元素的元组
}
print(flexible_dict["numbers"]) # Output: [1, 2, 3]
print(flexible_dict["nested"]["a"]) # Output: 1我们会在第 17 章更深入地探讨可哈希性,但现在请记住:使用不可变类型(字符串、数字、元组)作为键,你就不会出错。
16.2) 添加与更新字典条目
16.2.1) 添加新的键值对
向字典中添加新条目很直接——只需要给一个新键赋值:
grades = {"Alice": 95, "Bob": 87}
print(grades) # Output: {'Alice': 95, 'Bob': 87}
# 添加一名新学生
grades["Charlie"] = 92
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
# 再添加一名学生
grades["Diana"] = 88
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}如果键不存在,Python 会创建它。如果键已存在,Python 会更新其值(我们下一节会讲)。
16.2.2) 更新已有值
要更新某个值,只需要给一个已存在的键赋新值:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
# 更新 Bob 的成绩
grades["Bob"] = 90
print(grades) # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92}
# 更新多个成绩
grades["Alice"] = 98
grades["Charlie"] = 94
print(grades) # Output: {'Alice': 98, 'Bob': 90, 'Charlie': 94}Python 并不区分“添加”和“更新”——语法完全相同。如果键存在就更新值;如果不存在就创建新条目。
下面是一个实际例子,同时展示添加与更新:
# 跟踪库存
inventory = {"apple": 50, "banana": 30}
print("Initial inventory:", inventory) # Output: Initial inventory: {'apple': 50, 'banana': 30}
# 补货苹果(更新已有键)
inventory["apple"] = inventory["apple"] + 20
print("After restocking apples:", inventory) # Output: After restocking apples: {'apple': 70, 'banana': 30}
# 添加新产品(添加新键)
inventory["orange"] = 40
print("After adding oranges:", inventory) # Output: After adding oranges: {'apple': 70, 'banana': 30, 'orange': 40}
# 卖出一些香蕉(更新已有键)
inventory["banana"] = inventory["banana"] - 10
print("After selling bananas:", inventory) # Output: After selling bananas: {'apple': 70, 'banana': 20, 'orange': 40}16.2.3) 使用 update() 合并字典
update() 方法可以一次性添加多个键值对,或将另一个字典合并到当前字典中:
grades = {"Alice": 95, "Bob": 87}
print(grades) # Output: {'Alice': 95, 'Bob': 87}
# 一次性添加多个学生
new_students = {"Charlie": 92, "Diana": 88}
grades.update(new_students)
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
# 更新已有键并添加新键
more_updates = {"Bob": 90, "Eve": 85} # Bob 的成绩改变,Eve 是新增
grades.update(more_updates)
print(grades) # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92, 'Diana': 88, 'Eve': 85}update() 方法会原地修改字典。如果键已存在,就更新其值;如果不存在,就添加该键值对。
你也可以给 update() 传入关键字参数:
student = {"name": "Alice", "age": 20}
print(student) # Output: {'name': 'Alice', 'age': 20}
# 使用关键字参数更新
student.update(age=21, major="Computer Science")
print(student) # Output: {'name': 'Alice', 'age': 21, 'major': 'Computer Science'}下面是一个合并配置设置的实际例子:
# 默认设置
config = {
"theme": "light",
"font_size": 12,
"auto_save": True
}
# 用户偏好(覆盖部分默认值)
user_prefs = {
"theme": "dark",
"font_size": 14
}
# 将用户偏好合并到 config 中
config.update(user_prefs)
print(config) # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}16.2.4) 使用 setdefault() 仅在键不存在时添加
当你只想在键不存在时添加键值对,setdefault() 方法就很有用。如果键已存在,它会返回当前值且不做修改:
grades = {"Alice": 95, "Bob": 87}
# 添加 Charlie(键不存在)
result = grades.setdefault("Charlie", 90)
print(result) # Output: 90
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90}
# 尝试添加 Alice(键已存在 - 不改变)
result = grades.setdefault("Alice", 80)
print(result) # Output: 95 (existing value returned)
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90} (unchanged)当你想确保所有必需的配置键都存在并具有默认值,同时保留用户已经自定义的值时,这尤其有用:
# 带默认值的应用配置
config = {"theme": "light", "font_size": 12}
# 确保所有必需设置都存在并带默认值
config.setdefault("auto_save", True)
config.setdefault("language", "en")
config.setdefault("theme", "dark") # 不会改变 - 已经存在
print(config)
# Output: {'theme': 'light', 'font_size': 12, 'auto_save': True, 'language': 'en'}
# 现在可以安全访问所有设置
print(f"Theme: {config['theme']}") # Output: Theme: light
print(f"Auto-save: {config['auto_save']}") # Output: Auto-save: True
print(f"Language: {config['language']}") # Output: Language: en16.3) 使用 del 和 pop() 删除字典条目
16.3.1) 使用 del 删除条目
del 语句会从字典中移除一个键值对:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
# 删除 Charlie
del grades["Charlie"]
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Diana': 88}
# 再删除一名学生
del grades["Bob"]
print(grades) # Output: {'Alice': 95, 'Diana': 88}如果你尝试删除一个不存在的键,Python 会抛出 KeyError:
grades = {"Alice": 95, "Bob": 87}
# 警告:KeyError - 仅用于演示
# del grades["Charlie"] # PROBLEM: KeyError: 'Charlie'要安全地删除一个可能不存在的键,可以先检查:
grades = {"Alice": 95, "Bob": 87}
# 安全删除
if "Charlie" in grades:
del grades["Charlie"]
else:
print("Charlie not found") # Output: Charlie not found16.3.2) 使用 pop() 删除并获取值
pop() 方法会删除一个键并返回它的值。当你既需要移除条目又需要使用它的值时,这很有用:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
# 删除并获取 Bob 的成绩
bob_grade = grades.pop("Bob")
print(bob_grade) # Output: 87
print(grades) # Output: {'Alice': 95, 'Charlie': 92}
# 删除并使用该值
charlie_grade = grades.pop("Charlie")
print(f"Charlie's final grade was {charlie_grade}") # Output: Charlie's final grade was 92
print(grades) # Output: {'Alice': 95}和 del 一样,如果键不存在,pop() 会抛出 KeyError。不过,你可以提供一个默认值来改为返回它:
grades = {"Alice": 95, "Bob": 87}
# 带默认值的 pop(键不存在)
diana_grade = grades.pop("Diana", 0)
print(diana_grade) # Output: 0
print(grades) # Output: {'Alice': 95, 'Bob': 87} (unchanged)
# 带默认值的 pop(键存在)
alice_grade = grades.pop("Alice", 0)
print(alice_grade) # Output: 95
print(grades) # Output: {'Bob': 87}16.3.3) 使用 clear() 删除所有条目
clear() 方法会移除字典中的所有键值对,使其变为空字典:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
# 清空所有条目
grades.clear()
print(grades) # Output: {}
print(len(grades)) # Output: 0这比重新赋值为空字典(grades = {})更明确,尤其是在其他变量也引用同一个字典时:
# 演示两者的区别
grades = {"Alice": 95, "Bob": 87}
backup = grades # backup 引用同一个字典
# 使用 clear() - 会影响两个变量
grades.clear()
print(grades) # Output: {}
print(backup) # Output: {} (same dictionary was cleared)
# 为下一个示例重置
grades = {"Alice": 95, "Bob": 87}
backup = grades
# 重新赋值 - 只影响 grades
grades = {}
print(grades) # Output: {}
print(backup) # Output: {'Alice': 95, 'Bob': 87} (different dictionary now)我们会在第 18 章讨论引用语义(reference semantics)时更深入地探索这种行为,但现在请记住:clear() 会清空现有字典,而重新赋值会创建一个新的空字典。
16.4) 字典视图对象:keys()、values() 和 items()
16.4.1) 理解字典视图
字典提供了三个方法,会返回视图对象(view objects)——一种特殊对象,用于提供字典键、值或键值对的动态视图。这些视图会自动反映字典的变化:
keys():返回所有键的视图values():返回所有值的视图items():返回所有键值对的视图(以元组形式)
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 获取视图
keys_view = grades.keys()
values_view = grades.values()
items_view = grades.items()
print(keys_view) # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
print(values_view) # Output: dict_values([95, 87, 92])
print(items_view) # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])这些不是列表——它们是视图对象。但如果需要,你可以把它们转换成列表:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 将视图转换为列表
keys_list = list(grades.keys())
values_list = list(grades.values())
items_list = list(grades.items())
print(keys_list) # Output: ['Alice', 'Bob', 'Charlie']
print(values_list) # Output: [95, 87, 92]
print(items_list) # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]16.4.2) 视图是动态的
视图对象会自动反映字典的变化:
grades = {"Alice": 95, "Bob": 87}
# 创建一个视图
keys_view = grades.keys()
print(keys_view) # Output: dict_keys(['Alice', 'Bob'])
# 修改字典
grades["Charlie"] = 92
print(keys_view) # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
# 删除一个条目
del grades["Bob"]
print(keys_view) # Output: dict_keys(['Alice', 'Charlie'])这种动态行为在你需要处理可能变化的字典内容时很有用。不过,如果你需要一个不会变化的快照,就把视图转换为列表:
grades = {"Alice": 95, "Bob": 87}
# 创建一个快照
keys_snapshot = list(grades.keys())
print(keys_snapshot) # Output: ['Alice', 'Bob']
# 修改字典
grades["Charlie"] = 92
print(keys_snapshot) # Output: ['Alice', 'Bob'] (unchanged)16.4.3) 使用 keys()
keys() 方法返回所有字典键的视图。这在你需要检查有哪些键存在或遍历键时很有用:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 获取所有键
keys = grades.keys()
print(keys) # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
# 检查某个键是否存在
if "Alice" in keys:
print("Alice is in the grade book") # Output: Alice is in the grade book
# 统计键数量
print(f"Number of students: {len(keys)}") # Output: Number of students: 3你也可以不调用 keys(),直接在字典上检查成员关系:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 这两种写法等价
if "Alice" in grades.keys():
print("Found (using keys())")
if "Alice" in grades:
print("Found (direct check)") # 这种更常见也更简洁
# Output:
# Found (using keys())
# Found (direct check)16.4.4) 使用 values()
values() 方法返回所有字典值的视图。当你需要处理值而不关心它们对应的键时,这很有用:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 获取所有值
values = grades.values()
print(values) # Output: dict_values([95, 87, 92, 88])
# 计算统计信息
total = sum(values)
count = len(values)
average = total / count
print(f"Total points: {total}") # Output: Total points: 362
print(f"Number of students: {count}") # Output: Number of students: 4
print(f"Average grade: {average}") # Output: Average grade: 90.5
# 找到最高分和最低分
print(f"Highest grade: {max(values)}") # Output: Highest grade: 95
print(f"Lowest grade: {min(values)}") # Output: Lowest grade: 8716.4.5) 使用 items()
items() 方法返回键值对的视图,每个键值对以元组形式呈现。这是最常用的视图,因为它同时提供键和值:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 获取所有键值对
items = grades.items()
print(items) # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])
# 转换为列表以更清晰地看到元组
items_list = list(items)
print(items_list) # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]
# 访问单个元组
first_item = items_list[0]
print(first_item) # Output: ('Alice', 95)
print(first_item[0]) # Output: Alice
print(first_item[1]) # Output: 95items() 视图在迭代时尤其有用,我们会在下一节详细讲解。这里先预览一下:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 处理每个键值对
for name, grade in grades.items():
print(f"{name}: {grade}")
# Output:
# Alice: 95
# Bob: 87
# Charlie: 9216.5) 遍历键、值与条目
16.5.1) 遍历键(默认行为)
当你直接用 for 循环遍历一个字典时,遍历的是它的键:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 遍历键(隐式)
for name in grades:
print(name)
# Output:
# Alice
# Bob
# Charlie这等价于遍历 grades.keys():
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 遍历键(显式)
for name in grades.keys():
print(name)
# Output:
# Alice
# Bob
# Charlie两种方式效果完全相同。隐式版本(不写 .keys())更常见也更简洁。
下面是一个实际例子,用键来访问值:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 找出通过的学生(grade >= 90)
passing_students = []
for name in grades:
if grades[name] >= 90:
passing_students.append(name)
print("Students who passed:", passing_students) # Output: Students who passed: ['Alice', 'Charlie']16.5.2) 遍历值
要只遍历值,可以使用 values():
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 遍历值
for grade in grades.values():
print(grade)
# Output:
# 95
# 87
# 92
# 88当你需要处理值但不关心它们对应的键时,这很有用:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 计算总分和平均分
total = 0
count = 0
for grade in grades.values():
total = total + grade
count = count + 1
average = total / count
print(f"Class average: {average}") # Output: Class average: 90.5
# 检查是否所有学生都通过
all_passed = True
for grade in grades.values():
if grade < 60:
all_passed = False
break
if all_passed:
print("All students passed!") # Output: All students passed!16.5.3) 使用 items() 遍历键值对
最常见且最有用的遍历模式是使用 items() 同时获得键和值:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 遍历键值对
for name, grade in grades.items():
print(f"{name} scored {grade}")
# Output:
# Alice scored 95
# Bob scored 87
# Charlie scored 92注意元组解包:for name, grade in grades.items()。每个条目都是一个形如 ("Alice", 95) 的元组,我们把它解包到两个变量中。这比通过元组下标访问更易读:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 不使用解包(可读性较差)
for item in grades.items():
print(f"{item[0]} scored {item[1]}")
# 使用解包(可读性更好)
for name, grade in grades.items():
print(f"{name} scored {grade}")
# Both produce the same output:
# Alice scored 95
# Bob scored 87
# Charlie scored 9216.5.4) 在迭代期间修改字典
警告:在遍历字典的同时修改其大小(添加或删除键)可能会导致错误或出现意外行为:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 警告:RuntimeError - 仅用于演示
# for name in grades:
# if grades[name] < 90:
# del grades[name] # PROBLEM: RuntimeError: dictionary changed size during iteration在现代 Python(3.7+)中,一旦你尝试改变字典大小,就会立刻抛出 RuntimeError。Python 会检测到修改并停止执行,以防出现不可预测的行为。
在较老的 Python 版本中,这可能会导致迭代器:
- 跳过本应处理的条目
- 重复处理同一个条目两次
- 产生不一致的结果
这就是为什么 Python 现在会通过清晰的错误信息快速失败。
如果你确实需要在迭代中修改字典,请遍历键的副本:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 安全:遍历键的副本
for name in list(grades.keys()):
if grades[name] < 90:
del grades[name]
print(grades) # Output: {'Alice': 95, 'Charlie': 92}或者新建一个字典,只保留你想要的条目:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# 构建一个只包含过滤后条目的新字典
high_grades = {}
for name, grade in grades.items():
if grade >= 90:
high_grades[name] = grade
print(high_grades) # Output: {'Alice': 95, 'Charlie': 92}第二种方式往往更清晰也更安全。我们会在第 35 章通过字典推导式(dictionary comprehensions)学习更优雅的做法。
16.6) 常用字典方法
16.6.1) 使用 in 和 not in 检查键
in 和 not in 运算符用于检查字典中是否存在某个键:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# 检查键是否存在
if "Alice" in grades:
print("Alice is in the grade book") # Output: Alice is in the grade book
if "David" not in grades:
print("David is not in the grade book") # Output: David is not in the grade book这是在访问值之前检查键是否存在的首选方式。相比使用 get() 并检查 None,它更易读也更 Pythonic:
grades = {"Alice": 95, "Bob": 87}
# 推荐:使用 in
if "Alice" in grades:
print(f"Alice's grade: {grades['Alice']}") # Output: Alice's grade: 95
# 另一种方式:使用 get() 并检查 None
if grades.get("Alice") is not None:
print(f"Alice's grade: {grades['Alice']}") # Output: Alice's grade: 9516.6.2) 使用 len() 获取条目数量
len() 函数会返回字典中键值对的数量:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(len(grades)) # Output: 3
# 空字典
empty = {}
print(len(empty)) # Output: 0
# 修改后
grades["Diana"] = 88
print(len(grades)) # Output: 4
del grades["Bob"]
print(len(grades)) # Output: 3这可以用来检查字典是否为空,或用于报告统计信息:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
if len(grades) == 0:
print("No students in grade book")
else:
total = sum(grades.values())
average = total / len(grades)
print(f"{len(grades)} students, average grade: {average}")
# Output: 4 students, average grade: 90.516.6.3) 使用 copy() 复制字典
copy() 方法会创建字典的浅拷贝(shallow copy)——也就是一个新的字典,包含相同的键值对:
original = {"Alice": 95, "Bob": 87}
duplicate = original.copy()
print(original) # Output: {'Alice': 95, 'Bob': 87}
print(duplicate) # Output: {'Alice': 95, 'Bob': 87}
# 修改副本
duplicate["Charlie"] = 92
print(original) # Output: {'Alice': 95, 'Bob': 87} (unchanged)
print(duplicate) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}这不同于简单赋值,后者会创建对同一个字典的引用:
original = {"Alice": 95, "Bob": 87}
reference = original # 不是拷贝 - 是同一个字典
# 通过引用修改
reference["Charlie"] = 92
print(original) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92} (changed!)
print(reference) # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}我们会在第 18 章详细探讨浅拷贝与深拷贝(deep copy)。现在请记住:当你想要一个独立的字典副本时,使用 copy()。
16.6.4) 使用 | 运算符合并字典(Python 3.9+)
Python 3.9 引入了 | 运算符来合并字典。| 会创建一个新字典,组合两个字典中的所有键。对于重复键,右侧的值会覆盖左侧的值。两个原字典都保持不变。
defaults = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
# 合并字典(user_prefs 覆盖 defaults)
config = defaults | user_prefs
print(config) # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}
# 原字典不变
print(defaults) # Output: {'theme': 'light', 'font_size': 12, 'auto_save': True}
print(user_prefs) # Output: {'theme': 'dark', 'font_size': 14}下面是另一个例子,展示它在合并多个来源的数据时如何派上用场:
# 来自两个供应商的产品信息
supplier_a = {
"laptop": {"price": 999.99, "stock": 15},
"mouse": {"price": 29.99, "stock": 50}
}
supplier_b = {
"laptop": {"price": 949.99, "stock": 20}, # 更好的价格和库存
"keyboard": {"price": 79.99, "stock": 30}
}
# 合并:对于相同产品,supplier_b 的数据会覆盖 supplier_a
combined = supplier_a | supplier_b
print(combined)
# Output: {'laptop': {'price': 949.99, 'stock': 20}, 'mouse': {'price': 29.99, 'stock': 50}, 'keyboard': {'price': 79.99, 'stock': 30}}
# 现在 laptop 使用 supplier_b(更划算),mouse 使用 supplier_a,keyboard 使用 supplier_b|= 运算符会原地合并(修改左侧字典):
config = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
# 原地合并
config |= user_prefs
print(config) # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}这等价于使用 update(),但更简洁:
config = {"theme": "light", "font_size": 12}
user_prefs = {"theme": "dark", "font_size": 14}
# 这些写法等价
config.update(user_prefs)
# config |= user_prefs
print(config) # Output: {'theme': 'dark', 'font_size': 14}16.7) 用嵌套字典表示结构化数据
16.7.1) 创建嵌套字典
为什么要使用嵌套字典? 想象你要跟踪学生信息。你可以创建多个独立字典:ages = {"Alice": 20, "Bob": 19}、majors = {"Alice": "CS", "Bob": "Math"}、gpas = {"Alice": 3.8, "Bob": 3.6}。但这会变得难以管理——你必须让三个字典保持同步,而且要查一个学生的所有信息,需要进行三次独立查找。嵌套字典通过把相关数据组合在一起解决了这个问题:一个学生姓名通过一次查找就能映射到其全部信息。
嵌套字典(nested dictionary)是指字典的值中包含其他字典。这对于表示结构化、层级化的数据非常有用:
# 具有多个属性的学生记录
students = {
"Alice": {
"age": 20,
"major": "Computer Science",
"gpa": 3.8
},
"Bob": {
"age": 19,
"major": "Mathematics",
"gpa": 3.6
},
"Charlie": {
"age": 21,
"major": "Physics",
"gpa": 3.9
}
}
print(students)
# Output: {'Alice': {'age': 20, 'major': 'Computer Science', 'gpa': 3.8}, 'Bob': {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}, 'Charlie': {'age': 21, 'major': 'Physics', 'gpa': 3.9}}每个学生姓名都映射到另一个字典,里面包含他们的属性。相比为每个属性使用独立字典,这种结构更灵活也更易维护。
16.7.2) 访问嵌套值
要访问嵌套值,请使用多层方括号:
students = {
"Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
"Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
# 访问嵌套值
alice_age = students["Alice"]["age"]
print(alice_age) # Output: 20
bob_major = students["Bob"]["major"]
print(bob_major) # Output: Mathematics
# 用在表达式中
print(f"Alice is {students['Alice']['age']} years old")
# Output: Alice is 20 years old
print(f"Bob's GPA: {students['Bob']['gpa']}")
# Output: Bob's GPA: 3.6每一层方括号都会访问一层嵌套。先用 students["Alice"] 获取内部字典,再用 ["age"] 从该字典中取出年龄值。
16.7.3) 安全访问嵌套值
在访问嵌套字典时,你需要检查每一层是否存在:
students = {
"Alice": {"age": 20, "major": "Computer Science"},
"Bob": {"age": 19} # Bob 还没有声明专业
}
# 不安全访问 - 可能抛出 KeyError
# print(students["Bob"]["major"]) # PROBLEM: KeyError: 'major'
# 使用多重检查进行安全访问
if "Bob" in students:
if "major" in students["Bob"]:
print(f"Bob's major: {students['Bob']['major']}")
else:
print("Bob hasn't declared a major") # Output: Bob hasn't declared a major
# 使用 get() 进行安全的嵌套访问
bob_major = students.get("Bob", {}).get("major", "Undeclared")
print(f"Bob's major: {bob_major}") # Output: Bob's major: Undeclaredget() 链之所以能工作,是因为如果 "Bob" 不存在,get() 会返回一个空字典 {},然后第二个 get() 就会安全地返回 "Undeclared"。
16.7.4) 修改嵌套字典
你可以在嵌套字典中添加、更新或删除条目:
students = {
"Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
"Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
# 更新一个嵌套值
students["Alice"]["gpa"] = 3.9
print(f"Alice's new GPA: {students['Alice']['gpa']}") # Output: Alice's new GPA: 3.9
# 给已有学生添加一个新属性
students["Bob"]["email"] = "bob@university.edu"
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6, 'email': 'bob@university.edu'}
# 添加一个带嵌套数据的新学生
students["Charlie"] = {
"age": 21,
"major": "Physics",
"gpa": 3.7
}
print(f"Number of students: {len(students)}") # Output: Number of students: 3
# 删除一个属性
del students["Bob"]["email"]
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}16.7.5) 遍历嵌套字典
你可以使用嵌套循环遍历嵌套字典:
students = {
"Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
"Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
"Charlie": {"age": 21, "major": "Physics", "gpa": 3.9}
}
# 遍历学生及其属性
for name, info in students.items():
print(f"\n{name}:")
for key, value in info.items():
print(f" {key}: {value}")
# Output:
# Alice:
# age: 20
# major: Computer Science
# gpa: 3.8
#
# Bob:
# age: 19
# major: Mathematics
# gpa: 3.6
#
# Charlie:
# age: 21
# major: Physics
# gpa: 3.9下面是一个实际例子,找出满足特定条件的学生:
students = {
"Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
"Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
"Charlie": {"age": 21, "major": "Physics", "gpa": 3.9},
"Diana": {"age": 20, "major": "Computer Science", "gpa": 3.5}
}
# 找出 GPA 高于 3.7 的 CS 学生
print("High-performing CS students:")
for name, info in students.items():
if info["major"] == "Computer Science" and info["gpa"] >= 3.7:
print(f" {name} (GPA: {info['gpa']})")
# Output:
# High-performing CS students:
# Alice (GPA: 3.8)字典是 Python 最强大、最通用的数据结构之一。它们提供快速查找、灵活组织方式,并能优雅地解决常见编程问题。随着你继续学习 Python,你会发现字典无处不在——从配置设置到数据处理,再到构建复杂应用。
本章涵盖的模式——计数、分组、查找表以及数据转换——构成了在 Python 中处理结构化数据的基础。在下一章中,我们将探索集合(set),另一种集合类型,它与字典互补,适用于处理唯一、无序的数据。