You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

继承dict实现ConfigDict时pickle反序列化丢失_filename属性问题

解决ConfigDict查询时触发的AttributeError问题

嘿,我一眼就看到问题出在哪了——你踩了pickle和dict子类结合的一个经典坑!

问题回顾

你继承dict实现了一个能持久化的配置字典ConfigDict,需求是通过命令行参数实现键值对的设置、查询和全量展示。代码能正常写入数据,但查询指定键时触发了如下错误:

AttributeError: 'ConfigDict' object has no attribute '_filename'
(堆栈跟踪指向__setitem__方法中的with open(self._filename,"wb") as fh:行)

你的代码如下:

import pickle,os,sys
class ConfigDict(dict):
    def __init__(self, filename):
        self._filename = filename
        if not os.path.exists(self._filename):
            with open(self._filename,"wb") as fh:
                pickle.dump({}, fh)
        with open(self._filename,"rb") as fh:
            self.update(pickle.load(fh))
    def __setitem__(self, key, value):
        dict.__setitem__(self,key,value)
        with open(self._filename,"wb") as fh:
            pickle.dump(self, fh)
    def __getitem__(self,key):
        return dict.__getitem__(self,key)
cd = ConfigDict('first.pickle')
# if 2 arguments on the command line,
# set a key and value in the object's dictionary
if len(sys.argv) == 3:
    key, value = sys.argv[1], sys.argv[2]
    print('writing data: {0}, {1}'.format(key, value))
    cd[key] = value
# if 1 argument on the command line, treat it as a key and show the value
elif len(sys.argv) == 2:
    print('reading a value')
    key = sys.argv[1]
    print('the value for {0} is {1}'.format(sys.argv[1], cd[key]))
# if no arguments on the command line, show all keys and values
else:
    print('keys/values:')
    for key in cd.keys():
        print(' {0} = {1}'.format(key, cd[key]))

问题根源

你在__setitem__里用pickle.dump(self, fh)把整个ConfigDict实例序列化到文件,但dict作为内置类型,pickle对它的子类序列化有特殊逻辑:

  1. 默认只会保存字典的键值对,不会保存子类自定义的属性(比如你的_filename);
  2. 反序列化时会绕过__init__方法直接重建对象,导致反序列化出来的ConfigDict实例完全没有_filename属性。

当你第二次运行程序查询时,__init__加载出的是这个残缺的实例,虽然你用self.update(...)把键值对复制到了当前正常的cd对象,但这个残缺实例如果被意外触发(比如隐式操作),就会抛出AttributeError。更关键的是,你根本不需要序列化整个对象——保存字典的纯键值对就足够了!

解决方案

修改核心逻辑,只序列化字典的内容而非整个实例,同时规范dict子类的初始化流程:

import pickle, os, sys

class ConfigDict(dict):
    def __init__(self, filename):
        # 先调用父类构造方法,确保dict初始化完整
        super().__init__()
        self._filename = filename
        if not os.path.exists(self._filename):
            # 初始化时保存空字典
            with open(self._filename, "wb") as fh:
                pickle.dump({}, fh)
        # 加载文件中的普通字典并更新当前对象
        with open(self._filename, "rb") as fh:
            self.update(pickle.load(fh))
    
    def __setitem__(self, key, value):
        # 调用父类方法设置键值对
        super().__setitem__(key, value)
        # 只保存纯键值对字典,而非整个实例
        with open(self._filename, "wb") as fh:
            pickle.dump(dict(self), fh)
    
    def __getitem__(self, key):
        # 增加键不存在的友好提示
        if key not in self:
            raise KeyError(f"键 '{key}' 不存在")
        return super().__getitem__(key)

cd = ConfigDict('first.pickle')

if len(sys.argv) == 3:
    key, value = sys.argv[1], sys.argv[2]
    print(f'writing data: {key}, {value}')
    cd[key] = value
elif len(sys.argv) == 2:
    print('reading a value')
    key = sys.argv[1]
    try:
        print(f'the value for {key} is {cd[key]}')
    except KeyError as e:
        print(e)
else:
    print('keys/values:')
    for key, value in cd.items():
        print(f' {key} = {value}')

修改说明

  1. 序列化内容调整:把pickle.dump(self, fh)改为pickle.dump(dict(self), fh),确保文件中只保存纯键值对字典,避免子类属性丢失问题;
  2. 规范父类调用:添加super().__init__()调用dict的构造方法,让初始化流程更合规;
  3. 友好报错处理:在__getitem__中增加键不存在的判断,并在查询分支添加try-except,提升用户体验;
  4. 代码简洁优化:用f-string替代format写法,让代码更易读。

这样修改后,再测试设置、查询、全量展示的功能,就不会再触发AttributeError了!

内容的提问来源于stack exchange,提问作者fsociety

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:06:47