9
\$\begingroup\$

I found myself needed zip but for dictionaries, and wanted to know if there is any way I can make this shorter and more concise, and/or faster. Also I cannot figure out what the type hinting might be for the return (I would like to put it in the reStructuredText).

def dict_zip(*args):
    """The zip utility but for dictionaries

    :param args:
        A bunch of **identical** dicts
    :type args:
        dict
    :return:
        A dict of all the keys in the input dict, 
        mapped to a list of all the values fro that key
    :rtype:
        dict
    """
    out = {i: [] for i in args[0]}
    for dic in args:
        for key in out:
            out[key].append(dic[key])
    return out

if __name__ == "__main__":
    dict1 = {"first": True, "name": "John"}
    dict2 = {"first": False, "name": "sue"}
    print dict_zip(dict1, dict2)
\$\endgroup\$
10
\$\begingroup\$

A shorter version, functionally equivalent to yours:

def dict_zip(*dicts):
    return {k: [d[k] for d in dicts] for k in args[0].keys()}

That's assuming all dicts have the same keys, or more exactly, all dicts have at least all the keys present in the first dict.

To make it more robust and handle cases when dicts don't have the same keys:

def dict_zip(*dicts):
    all_keys = {k for d in dicts for k in d.keys()}
    return {k: [d[k] for d in dicts if k in d] for k in all_keys}

Regarding type hints: not sure, but it might be something based on typing.MutableMapping. But that's Python 3.

EDIT

To add padding for keys which are not present in all dicts:

def dict_zip(*dicts, fillvalue=None):
    all_keys = {k for d in dicts for k in d.keys()}
    return {k: [d.get(k, fillvalue) for d in dicts] for k in all_keys}

If you have to use Python 2 (if you can, upgrade to Py3 for your own good):

def dict_zip(*dicts, **kwargs):
    fillvalue = kwargs.get('fillvalue', None)
    all_keys = {k for d in dicts for k in d.keys()}
    return {k: [d.get(k, fillvalue) for d in dicts] for k in all_keys}

Notice that you could just do kwargs.get('fillvalue') and if 'fillvalue' is not in kwargs, get would return None anyways. But explicit is better than implicit.

\$\endgroup\$
  • 2
    \$\begingroup\$ Could I recommend to rename padding to fillvalue to match itertools.zip_longest? padding sounds more like string formatting than dict comprehension. \$\endgroup\$ – Adam Smith Apr 12 '17 at 22:19
  • 1
    \$\begingroup\$ @AdamSmith Thanks for the suggestion, renamed. \$\endgroup\$ – kyrill Apr 12 '17 at 22:20
  • 1
    \$\begingroup\$ Note that type hinting has been backported to Python2.7 and is available as a third party library called mypy. \$\endgroup\$ – Akshat Mahajan Apr 12 '17 at 23:10
6
\$\begingroup\$

You can simplify the initialization and allow for more versatile inputs (all the dictionaries does not need to have the same keys) by using defaultdicts:

from collection import defaultdict


def dict_zip(*args):
    output = defaultdict(list)
    for dictionary in args:
        for key, value in dictionary.items():
            output[key].append(value)
    return output
\$\endgroup\$
  • \$\begingroup\$ defaultdict was also the first thing that came to my mind when I saw the question :) +1 \$\endgroup\$ – яүυк Apr 12 '17 at 20:24
  • \$\begingroup\$ defaultdict is one of those things that has been missing my whole life, its amazing! Thanks for letting me know about it! \$\endgroup\$ – Mark Omo Apr 12 '17 at 20:32
  • \$\begingroup\$ I was writing this for generating pandas dataframes, how might I go about supporting entries that are not in every dict cleanly? I would want to do somthing like padding the entry in the list with None objects or somthing like that. \$\endgroup\$ – Mark Omo Apr 12 '17 at 21:03
  • 1
    \$\begingroup\$ @MarkOmo See my updated answer. \$\endgroup\$ – kyrill Apr 12 '17 at 21:20
3
\$\begingroup\$

Yet another fast solution including typing.

from itertools import chain
from typing import Any, Dict, Tuple

# To return values for each key in each dict
def dict_zip(*dicts: Tuple[Dict[Any, Any]], default: Any=None) -> Dict[Any, Tuple[Any]]:
    return {key: tuple(d.get(key, defualt) for d in dicts) for key in set(chain(*dicts))}

# To return values only for common keys
def dict_zip(*dicts: Tuple[Dict[Any, Any]]) -> Dict[Any, Tuple[Any]]:
    keys = set.intersection(*map(set, dicts))
    return {key: tuple(d[key] for d in dicts) for key in keys}
\$\endgroup\$
  • \$\begingroup\$ You misspelled default on line 6. Also chain.from_iterables(..) should be better than chain(*..). \$\endgroup\$ – Mathias Ettinger Apr 13 '17 at 20:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.