Python 字典嵌套与引用陷阱:动态更新内部字典的正确姿势

DDD
发布: 2025-10-26 12:44:14
原创
346人浏览过

Python 字典嵌套与引用陷阱:动态更新内部字典的正确姿势

本教程深入探讨了在python中构建嵌套字典时,因可变对象引用导致的常见陷阱。当尝试迭代更新内部字典并将其赋值给外部字典时,不当操作可能导致所有外部字典的键最终引用同一个内部字典的最新状态。文章提供了两种核心解决方案:使用 `dict.copy()` 进行浅拷贝,或在每次迭代中重新初始化内部字典,确保每个外部字典键都指向一个独立的内部字典实例。

Python中嵌套字典的引用问题

在Python编程中,字典(dict)是一种非常灵活的数据结构,常用于存储键值对。当我们需要处理更复杂的数据时,嵌套字典(即字典的值是另一个字典)变得尤为有用。然而,在动态构建或更新这类嵌套结构时,一个常见的陷阱是由于Python中可变对象的引用机制而导致的数据覆盖问题。

假设我们有一个初始字典 data_template,其结构如下:

data_template = {
    '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'}
}
登录后复制

我们的目标是遍历 data_template 的每个顶级键,并根据外部数据源(例如Excel文件,使用 openpyxl 库读取)中的相应行来填充每个内部字典的 'Name'、'Code' 等字段。最终期望得到一个 newest_dict,其中每个顶级键都映射到其特有的、从Excel读取的数据。

问题现象

立即学习Python免费学习笔记(深入)”;

在实际操作中,如果采用以下代码逻辑,可能会遇到所有顶级键最终都指向同一个内部字典的最新数据的问题:

import openpyxl
import datetime

# 模拟初始数据和Excel工作表
data_template = {
    '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'}
}

# 模拟 openpyxl 工作表 (ws)
# 假设 Excel 文件 'data.xlsx' 存在,并且内容如下:
# |   | A                          | B                              | C                | D                |
# |---|----------------------------|--------------------------------|------------------|------------------|
# | 1 | Header_Name                | Header_Code                    | Header_SaleStart | Header_SaleEnd   |
# | 2 | LG G7 Blue 64GB            | LG_G7_Blue_64GB_R07            | 2005-09-25       | 2022-10-27       |
# | 3 | Asus ROG Phone Nero 128GB  | Asus_ROG_Phone_Nero_128GB_R07  | 2005-09-25       | 2022-10-27       |

# 为了代码可运行,这里手动模拟 ws[cell].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):
        class CellValue:
            def __init__(self, value):
                self.value = value
            def __str__(self):
                return str(self.value)
        return CellValue(self.data.get(key, None))

ws = MockWorksheet()

new_dict = {}
newest_dict = {}
row = 2

for k, v in data_template.items():
    # v 是 {'Name': 'A', 'Code': 'B', ...}
    for i, j in v.items():
        # j 是 'A', 'B', 'C', 'D'
        # ws[j+str(row)].value 会从 Excel 读取相应单元格的值
        cell_value = ws[j + str(row)].value
        new_dict[i] = cell_value # 更新 new_dict

    print(f"--- 迭代键: {k} ---")
    print(f"当前 new_dict: {new_dict}")
    print("--------------------")

    newest_dict[k] = new_dict # <--- 问题所在:这里存储的是 new_dict 的引用
    print(f"当前 newest_dict: {newest_dict}")
    row += 1

print("\n最终 newest_dict:")
print(newest_dict)
登录后复制

运行上述代码,你会发现 newest_dict 的输出结果类似:

{'LG_G7_Blue_64GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', 'Sale Effective Date': datetime.datetime(2005, 9, 25, 0, 0), 'Sale Expiration Date': datetime.datetime(2022, 10, 27, 23, 59, 59)}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', 'Sale Effective Date': datetime.datetime(2005, 9, 25, 0, 0), 'Sale Expiration Date': datetime.datetime(2022, 10, 27, 23, 59, 59)}}
登录后复制

可以看到,'LG_G7_Blue_64GB_R07' 键下的值竟然是 'Asus ROG Phone Nero 128GB' 的数据,这显然不是我们期望的结果。两个顶级键都指向了最后一次迭代时 new_dict 的内容。

核心原因分析

这个问题的根源在于Python中可变对象(如字典、列表)的赋值是“引用传递”。当执行 newest_dict[k] = new_dict 时,并不是将 new_dict 的当前内容复制一份给 newest_dict[k],而是让 newest_dict[k] 指向了 new_dict 这个同一个对象

在循环的第一次迭代中,new_dict 被填充了 'LG_G7_Blue_64GB_R07' 的数据,然后 newest_dict['LG_G7_Blue_64GB_R07'] 指向了这个 new_dict 对象。 在第二次迭代中,new_dict 被清空(虽然这里没有显式清空,但其内容会被新的键值对覆盖)并填充了 'Asus_ROG_Phone_Nero_128GB_R07' 的数据。由于 newest_dict['LG_G7_Blue_64GB_R07'] 和 newest_dict['Asus_ROG_Phone_Nero_128GB_R07'] 都指向了同一个 new_dict 对象,所以当 new_dict 在第二次迭代中被修改后,所有指向它的引用都会看到这些修改,导致它们最终都显示 new_dict 在循环结束时的状态。

解决方案

为了解决这个问题,我们需要确保在每次将 new_dict 赋值给 newest_dict[k] 时,都是传递一个独立的副本,而不是同一个对象的引用。这里提供两种常用的解决方案。

方案一:使用 dict.copy() 进行浅拷贝

dict.copy() 方法会创建一个新的字典,其中包含原始字典的键值对的浅拷贝。这意味着新字典中的键值对是独立的,对新字典的修改不会影响原始字典,反之亦然。

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online 193
查看详情 Find JSON Path Online

修改后的代码示例:

import openpyxl
import datetime

# 模拟初始数据和Excel工作表 (同上)
data_template = {
    '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'}
}

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):
        class CellValue:
            def __init__(self, value):
                self.value = value
            def __str__(self):
                return str(self.value)
        return CellValue(self.data.get(key, None))

ws = MockWorksheet()

new_dict = {}
newest_dict = {}
row = 2

for k, v in data_template.items():
    for i, j in v.items():
        cell_value = ws[j + str(row)].value
        new_dict[i] = cell_value

    print(f"--- 迭代键: {k} ---")
    print(f"当前 new_dict: {new_dict}")
    print("--------------------")

    newest_dict[k] = new_dict.copy() # <--- 关键修改:使用 .copy()
    print(f"当前 newest_dict: {newest_dict}")
    row += 1

print("\n最终 newest_dict:")
print(newest_dict)
登录后复制

输出结果(符合预期):

{'LG_G7_Blue_64GB_R07': {'Name': 'LG G7 Blue 64GB', 'Code': 'LG_G7_Blue_64GB_R07', 'Sale Effective Date': datetime.datetime(2005, 9, 25, 0, 0), 'Sale Expiration Date': datetime.datetime(2022, 10, 27, 23, 59, 59)}, 'Asus_ROG_Phone_Nero_128GB_R07': {'Name': 'Asus ROG Phone Nero 128GB', 'Code': 'Asus_ROG_Phone_Nero_128GB_R07', 'Sale Effective Date': datetime.datetime(2005, 9, 25, 0, 0), 'Sale Expiration Date': datetime.datetime(2022, 10, 27, 23, 59, 59)}}
登录后复制

现在,每个顶级键都正确地关联了其独特的数据。

注意事项: dict.copy() 执行的是浅拷贝。如果 new_dict 内部的值本身也是可变对象(例如列表或另一个字典),那么这些内部的可变对象在拷贝后仍然是引用共享的。对于本例,new_dict 的值是字符串、日期时间对象等不可变类型,因此浅拷贝足够。如果需要更深层次的独立性,则需要使用 copy 模块的 deepcopy() 方法。

方案二:在循环内部重新初始化内部字典

另一种有效的解决方案是将 new_dict = {} 的初始化语句移动到外层 for 循环的内部。这样,在每次迭代开始时,都会创建一个全新的空字典 new_dict,从而确保每次赋值给 newest_dict[k] 的都是一个独立的字典对象。

修改后的代码示例:

import openpyxl
import datetime

# 模拟初始数据和Excel工作表 (同上)
data_template = {
    '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'}
}

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):
        class CellValue:
            def __init__(self, value):
                self.value = value
            def __str__(self):
                return str(self.value)
        return CellValue(self.data.get(key, None))

ws = MockWorksheet()

newest_dict = {}
row = 2

for k, v in data_template.items():
    new_dict = {} # <--- 关键修改:在每次外层循环开始时重新初始化 new_dict
    for i, j in v.items():
        cell_value = ws[j + str(row)].value
        new_dict[i] = cell_value

    print(f"--- 迭代键: {k} ---")
    print(f"当前 new_dict: {new_dict}")
    print("--------------------")

    newest_dict[k] = new_dict # 现在这里赋值的是每次迭代新创建的 new_dict 对象
    print(f"当前 newest_dict: {newest_dict}")
    row += 1

print("\n最终 newest_dict:")
print(newest_dict)
登录后复制

此方案同样能得到与方案一相同的正确输出结果。

总结与最佳实践

在Python中处理嵌套的可变数据结构时,理解变量赋值的引用行为至关重要。当一个可变对象(如字典或列表)被赋值给多个变量或作为另一个数据结构的值时,它们可能共享同一个底层对象。对其中一个引用的修改会反映在所有其他引用上。

为了避免这种意外的数据覆盖,我们可以采取以下策略:

  1. 使用 copy() 方法进行浅拷贝:当你需要一个字典的独立副本,且其内部的值都是不可变类型,或者即使是可变类型但你确定不需要对内部可变对象进行独立修改时,dict.copy() 是一个简洁高效的选择。
  2. 在循环内部重新初始化可变对象:当你在循环中构建或填充一个内部可变对象,并希望每次迭代都生成一个全新的实例时,将该对象的初始化语句放在循环内部是确保独立性的直接方法。
  3. 考虑 copy.deepcopy() 进行深拷贝:如果你的嵌套数据结构包含多层可变对象,并且你需要确保所有层级的对象都是完全独立的(即对任何层级的修改都不会影响原始结构),那么 import copy 并使用 copy.deepcopy() 是最安全的做法。

通过理解并恰当应用这些技术,你可以更有效地构建和管理复杂的Python数据结构,避免常见的引用陷阱,确保程序的行为符合预期。

以上就是Python 字典嵌套与引用陷阱:动态更新内部字典的正确姿势的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号