Python dict with default value based on key
Asked Answered
E

5

6

I need a dictionary that is automatically filled with a default value for each accessed key that is missing. I've found defaultdict and some other ways to achieve this, but the problem in my case is that I want the default value for each key to be specific to the key itself.

For example, with defaultdict I can achieve something like this:

from collections import defaultdict
d = defaultdict(lambda: 5)
> d[1] = 3
> d[1]
> 3
> d[2]
> 5

But what if I need the default value for each accessed missing key to be for example key + 5? Something like:

from collections import defaultdict
d = defaultdict(lambda key: key + 5)  # <-- This does not work as defaultdict expects lambda function to be without any parameters
> d[1] = 3
> d[1]
> 3
> d[2]
> 7         <- Calculated from accessed key + 5 (2+5)
> d[5]
> 10        <- Calculated from accessed key + 5 (5+5)

Is there a clean, builtin way to achieve what I need? I know that I can create a subclass of dict and implement this specific functionality at __getitem__ level, but I want to avoid that if possible.

I couldn't find a solution in other answers, so sorry if it is still a duplicate.

Eleph answered 26/1, 2021 at 12:59 Comment(4)
Why do you think your current way is not clean?Territus
defaultdict only works with lambda functions without parameters, so the second example is not a working python code.Eleph
It's just showing what kind of functionality I want to end up withEleph
Oh, OK. I thought this is the current behavior and you want further improvements.Territus
B
8

I don't think there is a builtin way of doing this. However, instead of subclassing dict and change getitem, you can subclass defaultdict itself to tell __missing__() to call the default_factory with an argument (the key), rather than without args. It is still a custom class, not a builtin, but it should be quite efficient still.

from collections import defaultdict

class DefaultDict(defaultdict):
    def __missing__(self, key):
        return self.default_factory(key)

Then:

d = DefaultDict(lambda key: key + 5)

d[2]
# 7
Bubal answered 26/1, 2021 at 13:25 Comment(1)
Even though I wanted to avoid creation of a subclass, this seems like the best solution for now, so I'll accept this answer, thanks!Eleph
P
1

You can also use a function like this

dic = {}
DEFAULT_VALUE = 5

def dict_get(item):
    try:
        return [dic[item]]
    except:
        dic[int(item)] = DEFAULT_VALUE + int(item)
        return DEFAULT_VALUE + int(item)

print(dict_get(10))
Peshitta answered 26/1, 2021 at 13:29 Comment(1)
The problem with this approach is that dic is searched twice for the key (if the key is missing): first time it searches for it when you try to return, then it searches again when you insert the key (it's the default behaviour: whenever you try to insert a key the dict first checks if the key is already present or not). If the dict is huge, it matters. defauldict from the standard library avoids this double search.Askance
S
1

Another simpler way, not relying on defaultdict

Sub classing dict and relying on overriding the __missing__ method

class DefaultDict(dict):
    def __init__(self, key_fn):
        super().__init__()
        self._key_fn = key_fn

    def __missing__(self, key):
        v = self._key_fn(key)
        self[key] = v
        return v

d = DefaultDict(lambda key: f"{key}-is-the-key")

puts(d[2])

for k, v in d.items():
    print(k, v)

Or if you want with python typing

K = TypeVar('K')
V = TypeVar('V')


class DefaultDict(dict[K, V]):
    def __init__(self, key_fn: Callable[[K], V]):
        super().__init__()
        self._key_fn = key_fn

    def __missing__(self, key: K) -> V:
        v = self._key_fn(key)
        self[key] = v
        return v

d: DefaultDict[int, str] = DefaultDict(lambda key: f"this-is-the-{key}")

print(d[2])
Snarl answered 29/8 at 12:21 Comment(0)
R
0

Here is how to do it without creating any class. You can use dict.setdefault:

d = {}
d_default_factory = lambda key: key + 5
d.setdefault(2, d_default_factory(2))
# 7

Optionally, you could save the default factory as an entry in the dictionary, like so:

d = {'default_factory': lambda key: key + 5}
d.setdefault(2, d['default_factory'](2))
# 7
Rebec answered 26/10, 2022 at 0:27 Comment(0)
C
0

(I do not have enough reputation to comment, so I am commenting here)

Pierre's top answer does work for setting and getting values in the defaultdict, but it does not add the k-v pair to the dict, so you cannot iterate through all the keys, values, or items.

Instead, add self[key] = self.default_factory(key)

from collections import defaultdict

class DefaultDict(defaultdict):
    def __missing__(self, key):
        self[key] = self.default_factory(key)
        return self[key]
Canonicate answered 31/5 at 6:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.