Home > OS >  Overriding the `[]` operator in a dictionary of dictionaries
Overriding the `[]` operator in a dictionary of dictionaries

Time:06-20

I am trying to implement a class which provides a dictionary with a default value:

from copy import deepcopy

class Dict:
    def __init__(self, default) -> None:
        self.default = default
        self.values = {}

    def __getitem__(self, key):
        return self.values[key] if key in self.values else deepcopy(self.default)

    def __setitem__(self, key, value):
        self.values[key] = value

It works as expected when the default value is "plain" (42 in the example below):

KEY = 'k'
d = Dict(42)
print(d[KEY]) # prints 42
d[KEY] = 53
print(d[KEY]) # prints 53

But it doesn't work as expected when the default value is by itself a Dict object:

KEY1 = 'k1'
KEY2 = 'k2'
d = Dict(Dict(42))
print(d[KEY1][KEY2]) # prints 42
d[KEY1][KEY2] = 53
print(d[KEY1][KEY2]) # prints 42

I have tried to debug that by adding various printouts within the class functions, but I haven't been able to figure it out.

What exactly am I doing wrong here?

CodePudding user response:

The immediate problem is in your __getitem__ method:

    def __getitem__(self, key):
        return self.values[key] if key in self.values else deepcopy(self.default)

Because you're only returning a value here, but not actually setting it, the returned value isn't useful. If you request a key that doesn't exist, the method is equivalent to:

def __getitem__(self, key):
    return deepcopy(self.default)

So when you write:

d[KEY1][KEY2] = 53

You're successfully setting a value for KEY2, but only in the dictionary returned by __getitem__. You probably want to use the dictionary setdefault method, which will set the key in self.values if it doesn't exist (in addition to returning it):

    def __getitem__(self, key):
        return self.values.setdefault(key, deepcopy(self.default))

With this implementation:

>>> KEY1 = 'k1'
>>> KEY2 = 'k2'
>>> d = Dict(Dict(42))
>>> print(d[KEY1][KEY2])
42
>>> d[KEY1][KEY2] = 53
>>> print(d[KEY1][KEY2])
53

But as I mentioned in my comment, a better solution is just to use the existing defaultdict implementation:

>>> from collections import defaultdict
>>> d = defaultdict(lambda: defaultdict(lambda: 42))
>>> d[KEY1][KEY2]
42
>>> d[KEY1][KEY2]=53
>>> d[KEY1][KEY2]
53

(The difference between defaultdict and the class you implemented is that the default must be a callable. Here's I've used lambda expressions, but you could also use actual functions, classes, etc).

CodePudding user response:

Since you are using deepcopy so it creates a copy without reference. You have to return the object without deepcopy.

def __getitem__(self, key): return self.values[key] if key in self.values else self.default

Now it should work as expected.

  • Related