
本文深入探讨了python中在创建嵌套字典时,由于对象引用特性可能导致所有外层字典键最终指向同一个内层字典实例的问题。通过具体代码示例,详细阐述了这一陷阱的成因,并提供了两种有效的解决方案:使用 `dict.copy()` 方法进行浅拷贝,以及在循环内部重新初始化内层字典,以确保每个外层键都拥有独立的内层字典副本。
引言:python字典的引用行为
在Python中,变量赋值并非复制值本身,而是复制对内存中对象的引用。对于不可变对象(如数字、字符串、元组),这通常不会引起混淆,因为一旦创建,它们的值就不能改变。然而,对于可变对象(如列表、字典、集合),当多个变量引用同一个可变对象时,通过任一变量修改该对象,所有引用该对象的变量都会看到这些修改。这种引用行为在处理嵌套数据结构时尤其需要注意,否则可能导致意想不到的结果。
问题重现:嵌套字典的引用陷阱
考虑一个常见的场景:我们有一个初始字典,其值是另一个字典,我们希望遍历这个初始字典,并根据外部数据源(例如excel文件)动态填充内部字典的值,最终构建一个新的嵌套字典。
假设我们有一个初始字典 initial_dict,结构如下:
initial_dict = { 'LG_G7_Blue_64GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'} }
我们希望从一个模拟的Excel工作表 ws 中读取数据,填充 Name、Code 等字段。以下是原始代码尝试实现此功能:
立即学习“Python免费学习笔记(深入)”;
import openpyxl import datetime # 模拟 openpyxl 的工作表和数据 # 在实际应用中,ws 会是一个已加载的 openpyxl 工作表对象 class MockCell: def __init__(self, value): self.value = value class MockWorksheet: def __init__(self): self.data = { 'A2': 'LG G7 Blue 64GB', 'B2': 'LG_G7_Blue_64GB_R07', 'C2': datetime.datetime(2005, 9, 25, 0, 0), 'D2': datetime.datetime(2022, 10, 27, 23, 59, 59), 'A3': 'Asus ROG Phone Nero 128GB', 'B3': 'Asus_ROG_Phone_Nero_128GB_R07', 'C3': datetime.datetime(2005, 9, 25, 0, 0), 'D3': datetime.datetime(2022, 10, 27, 23, 59, 59) } def __getitem__(self, key): return MockCell(self.data.get(key, None)) ws = MockWorksheet() # 初始字典结构 initial_dict = { 'LG_G7_Blue_64GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'} } new_dict = {} newest_dict = {} row = 2 for k, v in initial_dict.items(): for i, j in v.items(): # 从模拟的 Excel 工作表读取值 cell_ref = j + str(row) value_from_excel = ws[cell_ref].value new_dict[i] = value_from_excel print(f"处理键 '{k}' 后的 new_dict: {new_dict}") newest_dict[k] = new_dict # 问题所在:这里存储的是 new_dict 的引用 print(f"当前 newest_dict: {newest_dict}") print("------") row += 1 print("n最终结果 (原始问题代码):") print(newest_dict)
运行上述代码,你会发现最终 newest_dict 的输出并非预期。具体来说,’LG_G7_Blue_64GB_R07′ 和 ‘Asus_ROG_Phone_Nero_128GB_R07’ 这两个键所对应的内层字典值是相同的,都指向了最后一次迭代时 new_dict 的状态。
预期输出(部分):
{'LG_G7_Blue_64GB_R07': {'Name': 'LG G7 Blue 64GB', 'Code': 'LG_G7_Blue_64GB_R07', ...}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', ...}}
实际输出(部分):
{'LG_G7_Blue_64GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', ...}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', ...}}
问题分析: 问题的根源在于 new_dict = {} 在外层循环外部只被创建了一次。在每次外层循环迭代中,new_dict 的内容会被更新,但 newest_dict[k] = new_dict 语句仅仅是将 new_dict 这个字典对象的引用存储到了 newest_dict 中。因此,当 new_dict 在后续迭代中被修改时,所有指向它的引用(即 newest_dict 中的所有内层字典)都会反映这些修改,最终它们都指向了 new_dict 最后一次迭代后的状态。
解决方案一:使用 dict.copy() 进行浅拷贝
解决此问题的一种有效方法是在将 new_dict 赋值给 newest_dict 之前,创建一个 new_dict 的副本。Python 字典提供了 copy() 方法,用于执行浅拷贝。
import openpyxl import datetime # 模拟 openpyxl 的工作表和数据 (同上) class MockCell: def __init__(self, value): self.value = value class MockWorksheet: def __init__(self): self.data = { 'A2': 'LG G7 Blue 64GB', 'B2': 'LG_G7_Blue_64GB_R07', 'C2': datetime.datetime(2005, 9, 25, 0, 0), 'D2': datetime.datetime(2022, 10, 27, 23, 59, 59), 'A3': 'Asus ROG Phone Nero 128GB', 'B3': 'Asus_ROG_Phone_Nero_128GB_R07', 'C3': datetime.datetime(2005, 9, 25, 0, 0), 'D3': datetime.datetime(2022, 10, 27, 23, 59, 59) } def __getitem__(self, key): return MockCell(self.data.get(key, None)) ws = MockWorksheet() initial_dict = { 'LG_G7_Blue_64GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'} } new_dict = {} newest_dict = {} row = 2 print("n--- 解决方案一 (.copy()) 运行 ---") for k, v in initial_dict.items(): # new_dict 在循环外定义,每次迭代填充 # 但是在赋值给 newest_dict 时进行拷贝 for i, j in v.items(): cell_ref = j + str(row) value_from_excel = ws[cell_ref].value new_dict[i] = value_from_excel print(f"处理键 '{k}' 后的 new_dict: {new_dict}") newest_dict[k] = new_dict.copy() # 关键改动:使用 .copy() print(f"当前 newest_dict: {newest_dict}") print("------") row += 1 print("n最终结果 (解决方案一):") print(newest_dict)
通过将 newest_dict[k] = new_dict 改为 newest_dict[k] = new_dict.copy(),我们确保了每次迭代时,newest_dict 存储的是 new_dict 的一个独立副本,而不是其引用。这样,即使 new_dict 在后续迭代中被修改,之前存储的副本也不会受到影响。
注意事项: dict.copy() 执行的是浅拷贝。如果 new_dict 内部的值也是可变对象(例如嵌套列表或字典),那么这些嵌套的可变对象仍然是引用。在当前场景下,new_dict 的值是来自Excel的原始数据(字符串、日期时间对象等),它们通常是不可变或独立的对象,因此浅拷贝已足够。如果内部还有更深层的可变结构需要独立,则可能需要 copy.deepcopy()。
解决方案二:在循环内部重新初始化字典
另一种更简洁且通常更推荐的方法是,在每次外层循环迭代开始时,重新初始化 new_dict。这样可以确保每次迭代都从一个全新的空字典开始构建,从而自然地避免了引用问题。
import openpyxl import datetime # 模拟 openpyxl 的工作表和数据 (同上) class MockCell: def __init__(self, value): self.value = value class MockWorksheet: def __init__(self): self.data = { 'A2': 'LG G7 Blue 64GB', 'B2': 'LG_G7_Blue_64GB_R07', 'C2': datetime.datetime(2005, 9, 25, 0, 0), 'D2': datetime.datetime(2022, 10, 27, 23, 59, 59), 'A3': 'Asus ROG Phone Nero 128GB', 'B3': 'Asus_ROG_Phone_Nero_128GB_R07', 'C3': datetime.datetime(2005, 9, 25, 0, 0), 'D3': datetime.datetime(2022, 10, 27, 23, 59, 59) } def __getitem__(self, key): return MockCell(self.data.get(key, None)) ws = MockWorksheet() initial_dict = { 'LG_G7_Blue_64GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'A', 'Code': 'B', 'Sale Effective Date': 'C', 'Sale Expiration Date': 'D'} } newest_dict = {} row = 2 print("n--- 解决方案二 (内部重新初始化) 运行 ---") for k, v in initial_dict.items(): new_dict = {} # 关键改动:每次迭代都创建一个新的 new_dict for i, j in v.items(): cell_ref = j + str(row) value_from_excel = ws[cell_ref].value new_dict[i] = value_from_excel print(f"处理键 '{k}' 后的 new_dict: {new_dict}") newest_dict[k] = new_dict # 此时 new_dict 已经是新的对象,可以直接赋值 print(f"当前 newest_dict: {newest_dict}") print("------") row += 1 print("n最终结果 (解决方案二):") print(newest_dict)
将 new_dict = {} 移动到外层 for 循环内部,意味着在每次处理一个新的 initial_dict 键时,都会创建一个全新的 new_dict 对象。这样,newest_dict[k] = new_dict 语句就会存储对这个新创建的、独立的字典的引用,从而避免了引用冲突。这种方法通常被认为是更清晰、更符合逻辑的解决方案,因为它明确表达了“为每个外层键构建一个独立的内层字典”的意图。
总结与注意事项
在Python中处理可变数据结构(如字典和列表)的嵌套时,理解其引用行为至关重要。当将一个可变对象赋值给另一个变量或将其作为值存储在数据结构中时,通常是传递了对该对象的引用,而不是创建了一个独立的副本。
- 引用陷阱: 当在循环中重复使用同一个可变对象实例(如 new_dict)并将其赋值给另一个数据结构(如 newest_dict 的值)时,所有这些赋值最终都将指向同一个可变对象。当该对象在后续迭代中被修改时,所有引用都会看到这些修改。
- 解决方案一 (.copy()): 使用 dict.copy() 方法可以创建一个字典


