The code below works. The question is if there is a better and more elegant (maintainable) way.
The task is to update a Python dictionary which values are lists with another dictionary with the same structure (values are lists). The usual dict.update()
doesn't work because values (the lists) are replaced not updated (via list.extend()
).
Example:
a = {'A': [1, 2], 'C': [5]}
b = {'X': [8], 'A': [7, 921]}
Using dict.update()
would result in
{'A': [7, 921], 'C': [5], 'X': [8]}
But A
should become [1, 2, 7, 921]
.
Here is my working solution:
#!/usr/bin/env python3
from typing import Any
def update_dict_with_list_values(
a: dict[Any,list], b: dict[Any,list]) -> dict[Any,list]:
"""Update a dictionary and its list values from another dict."""
# create a local deep copy
a = a.copy()
for key in a:
try:
# remove the list
b_value = b.pop(key)
except KeyError:
pass
else:
# extend the list
a[key] = a[key] + b_value
# add the rest of keys that are not present in the dict to update
a.update(b)
return a
if __name__ == '__main__':
a = {
'A': [1, 2],
'C': [5]
}
b = {
'X': [8],
'A': [7, 921]
}
c = update_dict_with_list_values(a, b)
print(c) # -> {'A': [1, 2, 7, 921], 'C': [5], 'X': [8]}
3 Answers 3
Avoid conditionals by using defaultdict
. There is only one case: you want to extend
-merge lists from the second dictionary, with the target defaulting to an empty list.
from collections import defaultdict
from typing import Any, Iterable
def update_dict_with_list_values(
a: dict[Any, list], b: dict[Any, Iterable],
) -> dict[Any, list]:
"""Update a dictionary and its list values from another dict."""
union = defaultdict(list, a)
for k, values in b.items():
union[k].extend(values)
return union
This assumes that you don't mind mutating the value lists of a
, which is a risky assumption indeed. To avoid this you would deep-copy a
first, via something like
union = defaultdict(list, (
(k, list(v)) for k, v in a.items()
))
Or, for a very different approach which is naturally immune to side-effect concerns,
return {
k: a.get(k, []) + b.get(k, [])
for k in a.keys() | b.keys()
}
-
\$\begingroup\$ Because I don't understand
defaultdict
at all I don't get its advantage in the current case. \$\endgroup\$buhtz– buhtz2022年08月19日 05:51:07 +00:00Commented Aug 19, 2022 at 5:51 -
2\$\begingroup\$ @buhtz If you don't want
defaultdict
, you can use methoddict.setdefault
instead. That is, for a keyk
inb
, you can replaceif k in d: a[k].extend(b[k]) else: d[k] = list(b[k])
with the equivalenta.setdefault(k, []).extend(b[k])
. \$\endgroup\$Stef– Stef2022年08月19日 10:58:12 +00:00Commented Aug 19, 2022 at 10:58
Bottom line you need only 2 cases:
- if a
b
key is present ina
, merge values (extend for that matter) - if a
b
key is not present ina
, then assign it toa
Assuming you're only expecting lists as values, you could write something like this:
def update_dict_with_list_values(
a: dict[Any, list[Any]],
b: dict[Any, list[Any]],
) -> dict[Any, list[Any]]:
"""Update a dictionary and its list values from another dict."""
for key, value in b.items():
if key in a:
a[key].extend(value)
else:
a[key] = value
return a
I would recommend avoiding single-letter variable names!
Hope that helps!
-
\$\begingroup\$ Mhm... The code is easier to read but less efficient because of if statements. I tried to avoid them. And I would hypotize that in a short function like this it is acceptable. \$\endgroup\$buhtz– buhtz2022年08月18日 19:38:46 +00:00Commented Aug 18, 2022 at 19:38
-
3\$\begingroup\$ @buhtz Why do you think it's less efficient? Exception handling tends to be much slower than conditionals. \$\endgroup\$janos– janos2022年08月18日 20:07:26 +00:00Commented Aug 18, 2022 at 20:07
-
\$\begingroup\$ But exception only need to be handled when they occur not earlier. \$\endgroup\$buhtz– buhtz2022年08月18日 20:32:25 +00:00Commented Aug 18, 2022 at 20:32
-
\$\begingroup\$ You do not need two cases. \$\endgroup\$Reinderien– Reinderien2022年08月18日 22:54:47 +00:00Commented Aug 18, 2022 at 22:54
-
1\$\begingroup\$ Note that
a[key] = value
should probably be changed into the safera[key] = list(value)
, ora[key] = value.copy()
, otherwise future modifications ofb
might result in accidental modifications ofa
. \$\endgroup\$Stef– Stef2022年08月19日 11:00:06 +00:00Commented Aug 19, 2022 at 11:00
Based on that answer I suggest a solution avoiding if
. The key point here is that don't iterate over the dict to update (the target) but the dict that is updated with (the source).
def update_dict_with_list_values(
a: dict[Any,list], b: dict[Any,list]) -> dict[Any,list]:
"""Update a dictionary and its list values from another dict."""
for key, value in b.items():
try:
a[key].extend(value)
except KeyError:
a[key] = value
return a
-
2\$\begingroup\$ The
.copy()
function of adict
is not a deep copy, it's a shallow copy. \$\endgroup\$janos– janos2022年08月18日 20:10:32 +00:00Commented Aug 18, 2022 at 20:10 -
\$\begingroup\$ @RichardNeumann Just a typo. You are also able to edit and correct foreign answers by the way. ;) \$\endgroup\$buhtz– buhtz2022年08月19日 05:50:16 +00:00Commented Aug 19, 2022 at 5:50
-
1\$\begingroup\$ The line
a = copy.deepcopy(a)
should probably be removed; or if you really want to have it, should should add a warning in the docstring explaining thatupdate_dict_with_list_values
will make deepcopies and discard the original objects ina
. \$\endgroup\$Stef– Stef2022年08月19日 11:02:13 +00:00Commented Aug 19, 2022 at 11:02 -
1\$\begingroup\$ You are right. Using a copy would be more a
join_dict_with_list_values()
thenupdate...()
. \$\endgroup\$buhtz– buhtz2022年08月19日 11:03:27 +00:00Commented Aug 19, 2022 at 11:03 -
1\$\begingroup\$ Yes; but even for a
join
function, you should probably just writea = {k: list(v) for k,v in a.items()}
rather thana = copy.deepcopy(a)
, so as to make copies of the lists without making deepcopies of possible objects in the lists. (Also, in this case I'd recommend calling the return dictc = {k: list(v) for k,v in a.items()}
rather than giving it the same namea
, which is confusing and hides the fact that it's a local variable) \$\endgroup\$Stef– Stef2022年08月19日 11:04:42 +00:00Commented Aug 19, 2022 at 11:04