One of my favourite methods in ruby is Hash.slice. It returns a new hash containing only the specified keys from the original hash - very handy!
It works like this:
h = {a: 1, b: 2, c: 3, d: 4}
h.slice(:a, :b)
# => {a: 1, b: 2}
TL;DR, this is the basis of my python equivalent:
import operator as op
def slice_dict(d: dict, keys: list) -> dict:
return dict(zip(keys, op.itemgetter(*keys)(d)))
# however, you should just use dict comprehension:
def slice_dict(d: dict, keys: list) -> dict:
return {k: d[k] for k in keys if k in d}
slice
in PythonWe can definitely use a dictionary comprehension to achieve the same result:
def slice_dict(d: dict, keys: list) -> dict:
return {k: d[k] for k in keys if k in d}
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
slice_dict(d, ('a', 'b'))
# {'a': 1, 'b': 2}
We can use the dict
constructor to create a new dictionary from a list of tuples, which is a more efficient way to slice a dictionary:
def slice_dict(d: dict, keys: list) -> dict:
return dict((k, d[k]) for k in keys if k in d)
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
slice_dict(d, ('a', 'b'))
# {'a': 1, 'b': 2}
operator.itemgetter
We can use the [operator.itemgetter](https://docs.python.org/3/library/operator.html#operator.itemgetter)
function to create a callable that retrieves the specified keys from the dictionary.
First, a demonstration of how itemgetter
works:
import operator as op
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
getter = op.itemgetter('a', 'b')
# now we can use the getter to retrieve the values from a dictionary
getter(d)
# (1, 2)
We can then create a new dictionary by zipping the returned values with the keys:
keys = getter(d)
# (1, 2)
dict(zip(('a', 'b'), keys))
# {'a': 1, 'b': 2}
Combining these two steps, we can create a function that slices a dictionary:
import operator as op
def slice_dict(d: dict, keys: list) -> dict:
return dict(zip(keys, op.itemgetter(*keys)(d)))
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
slice_dict(d, ('a', 'b'))
# {'a': 1, 'b': 2}
If a key does not exist, then this will raise a KeyError
! e.g.
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
slice_dict(d, ('a', 'b', 'e'))
# KeyError: 'e'
To guard against this, we can use set operations to filter out any keys that don’t exist in the dictionary:
def slice_dict(d: dict, keys: list) -> dict:
keys = d.keys() & keys
return dict(zip(keys, op.itemgetter(*keys)(d)))
slice_dict(d, ('b', 'e', 'a'))
# {'a': 1, 'b': 2}
You might notice that the previous example returns the keys in the order they appear in the original dictionary, rather than the order defined in the keys
arg.
To preserve the order of the keys
arg, we can use a list comprehension to filter the keys instead:
def slice_dict(d: dict, keys: list) -> dict:
keys = [k for k in keys if k in d]
return dict(zip(keys, op.itemgetter(*keys)(d)))
Here are some quick benchmarks to compare the three methods:
I ran this in ipython so that I can use the %timeit
magic function
import operator as op
def slice_dict_comp(d: dict, keys: list) -> dict:
return {k: d[k] for k in keys if k in d}
def slice_dict_gen(d: dict, keys: list) -> dict:
return dict((k, d[k]) for k in keys if k in d)
def slice_dict_op(d: dict, keys: list) -> dict:
return dict(zip(keys, op.itemgetter(*keys)(d)))
def slice_dict_op_safe(d: dict, keys: list) -> dict:
keys = [k for k in keys if k in d]
return dict(zip(keys, op.itemgetter(*keys)(d)))
for fn in [slice_dict_comp, slice_dict_gen, slice_dict_op, slice_dict_op_safe]:
print(fn.__name__, fn(d, keys))
%timeit fn(d, keys)
function | time |
---|---|
slice_dict_comp | 147 ns |
slice_dict_gen | 410 ns |
slice_dict_op | 350 ns |
slice_dict_op_safe | 600 ns |