The problem is not pickle
but that your __getattr__
method breaks the expected contract by raising KeyError
exceptions. You need to fix your __getattr__
method to raise AttributeError
exceptions instead:
def __getattr__(self, item):
try:
return self[item]
except KeyError:
raise AttributeError(item)
Now pickle
is given the expected signal for a missing __getstate__
customisation hook.
From the object.__getattr__
documentation:
This method should return the (computed) attribute value or raise an AttributeError
exception.
(bold emphasis mine).
If you insist on keeping the KeyError
, then at the very least you need to skip names that start and end with double underscores and raise an AttributeError
just for those:
def __getattr__(self, item):
if isinstance(item, str) and item[:2] == item[-2:] == '__':
# skip non-existing dunder method lookups
raise AttributeError(item)
return self[item]
Note that you probably want to give your ddict()
subclass an empty __slots__
tuple; you don't need the extra __dict__
attribute mapping on your instances, since you are diverting attributes to key-value pairs instead. That saves you a nice chunk of memory per instance.
Demo:
>>> import pickle
>>> class ddict(dict):
... __slots__ = ()
... def __getattr__(self, item):
... try:
... return self[item]
... except KeyError:
... raise AttributeError(item)
... def __setattr__(self, key, value):
... self[key] = value
...
>>> pickle.dumps(ddict())
b'\x80\x03c__main__\nddict\nq\x00)\x81q\x01.'
>>> type(pickle.loads(pickle.dumps(ddict())))
<class '__main__.ddict'>
>>> d = ddict()
>>> d.foo = 'bar'
>>> d.foo
'bar'
>>> pickle.loads(pickle.dumps(d))
{'foo': 'bar'}
That pickle
tests for the __getstate__
method on the instance rather than on the class as is the norm for special methods, is a discussion for another day.