For each input i (a dict) in list **inputs**, evaluate f(**i)
    using multiprocessing if nprocs>1

    The result has the same format as the inputs: a list of dicts,
    taken from outputs, and updated with f(**i).
    If outputs is None, it is set to inputs.

def distribute_work(f, inputs, outputs=None, nprocs=1, out_key='output'):
    """
    For each input i (a dict) in list **inputs**, evaluate f(**i)
    using multiprocessing if nprocs>1

    The result has the same format as the inputs: a list of dicts,
    taken from outputs, and updated with f(**i).
    If outputs is None, it is set to inputs.
    """
    if outputs is None:
        outputs = [ip.copy() for ip in inputs]
    if nprocs <= 0:
        nprocs += multiprocessing.cpu_count()

    # no multiprocessing
    if nprocs <= 1:
        return [add_to_dict(op, f(**ip), key=out_key)
                for ip, op in zip(inputs, outputs)]

    # multiprocessing
    queue_in = multiprocessing.Queue()
    queue_out = multiprocessing.Queue()
    procs = [multiprocessing.Process(target=worker,
                                     args=(queue_in, queue_out, f))
             for _ in range(nprocs)]
    sent = [queue_in.put((i, args)) for i, args in enumerate(inputs)]
    [p.start() for p in procs]
    results = [queue_out.get() for _ in sent]
    for i, r in results:
        add_to_dict(outputs[i], r)
    [p.join() for p in procs]

    return outputs