HiveBrain v1.2.0
Get Started
← Back to all entries
snippetpythonMinor

Convert a dictionary of iterables into an iterator of dictionaries

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
iterablesiteratorconvertintodictionariesdictionary

Problem

I have a user-editable dictionary of content used to fill in a string.Template and I wanted to allow the user to fill the same template with several values at once.

Using only values, itertools.product would be enough for my needs but I needed dictionaries to fill in a template. I also wanted users to be able to enter a single string or numerical value as values for the dictionary without having anything blowing up and requiring them to encapsulate their single value in a 1-length tuple.

I used itertools.product(*map(pair_iter_safe, .items())) to generate iterables of key-value pairs suitable for the dict() constructor or the dict.update() method. With pair_iter_safe being:

def pair_iter_safe(kv_pair):
    key, value = kv_pair
    if isinstance(value, str):
        value = (value,)
    else:
        try:
            value = iter(value)
        except TypeError:
            value = (value,)

    for val in value:
        yield key, val


For a broader picture here is a simplified example of calling code:

```
import sys
import string
import itertools

PARAMETERS = {
'name': 'John Doe',
'age': range(40,45),
'activity': ('foo', 'bar', 'baz'),
}

SCENARIO = """\
[@campaign] @name:
Hi! I'm @age and I like to @activity.
"""

class AtTemplate(string.Template):
delimiter = '@'

def run_scenario(parameters, stdout):
scenario = AtTemplate(SCENARIO).substitute(parameters)
print(scenario, file=stdout)

def run_campaign(campaign_name='test', stdout=sys.stdout):
parameters = {'campaign': campaign_name}
for setup in itertools.product(*map(pair_iter_safe, PARAMETERS.items())):
parameters.update(setup)
run_scenario(parameters, stdout)

def pair_iter_safe(kv_pair):
key, value = kv_pair
if isinstance(value, str):
value = (value,)
else:
try:
value = iter(value)
except TypeError:
value = (value,)

for val in value:
yield key, val

if __name__

Solution

Interesting question.

I decided to look only at itertools.product(*map(pair_iter_safe, .items())) and pair_iter_safe.

The way I look at it is from a theoretic memory usage perspective. Due to how you have defined it, all (key, value) pairs need to be in memory at the same time, which could take a bit of memory.

I would suggest the following:

def iter_safe(value):
    if isinstance(value, str):
        value = (value,)
    try:
        iter(value)
    except TypeError:
        return (value,)
    else:
        return value


And using it as follows:

def dict_combinations(d):
    keys, values_list = zip(*d.items())
    for values in itertools.product(*map(iter_safe, values_list)):
        yield dict(zip(keys, values))


To me this is cleaner, having separated out the iter_safe logic from the dicts.

Though merely theoretical, I think this should save on memory usage due to there not being as much key-value pairs in memory all the time.

Of course, if you still want pair_iter_safe, you should be able to do something like

def pair_iter_safe(kv_pair):
    key, value = kv_pair
    for v in iter_safe(value):
        yield key, v

Code Snippets

def iter_safe(value):
    if isinstance(value, str):
        value = (value,)
    try:
        iter(value)
    except TypeError:
        return (value,)
    else:
        return value
def dict_combinations(d):
    keys, values_list = zip(*d.items())
    for values in itertools.product(*map(iter_safe, values_list)):
        yield dict(zip(keys, values))
def pair_iter_safe(kv_pair):
    key, value = kv_pair
    for v in iter_safe(value):
        yield key, v

Context

StackExchange Code Review Q#128088, answer score: 3

Revisions (0)

No revisions yet.