2

I have a dictionary with both string and 2-tuple keys. I want to convert all the 2-tuple keys from (x,y) to strings that are x:y. Here is my data:

In [4]:

data = {('category1', 'category2'): {'numeric_float1': {('Green', 'Car'): 0.51376354561039017,('Red', 'Plane'): 0.42304110216698415,('Yellow', 'Boat'): 0.56792298947973241}}}
data
Out[4]:
{('category1',
  'category2'): {'numeric_float1': {('Green', 'Car'): 0.5137635456103902,
   ('Red', 'Plane'): 0.42304110216698415,
   ('Yellow', 'Boat'): 0.5679229894797324}}}

However, this is the dictionary output I want:

{'category1:category2': 
    {'numeric_float1': 
        {'Green:Car': 0.5137635456103902,
         'Red:Plane': 0.42304110216698415,
         'Yellow:Boat': 0.5679229894797324}}}

I have altered code from a previous SO answer to create a recursive function that changes all the keys.

In [5]:

def convert_keys_to_string(dictionary):
    if not isinstance(dictionary, dict):
        return dictionary
    return dict((':'.join(k), convert_keys_to_string(v)) for k, v in dictionary.items())

convert_keys_to_string(data)

However I cannot get the function to avoid non-tuple keys. Because it does not avoid non-tuple keys, the function fixes the 2-tuple keys but messes up the non-tuple keys:

Out[5]:
{'category1:category2': {'n:u:m:e:r:i:c:_:f:l:o:a:t:1': {'Green:Car': 0.5137635456103902,
   'Red:Plane': 0.42304110216698415,
   'Yellow:Boat': 0.5679229894797324}}}
Community
  • 1
  • 1
Anton
  • 4,765
  • 12
  • 36
  • 50

3 Answers3

3

Change ':'.join(k) to k if hasattr(k, 'isalpha') else ':'.join(k). This will use the unaltered object if it has the attribute isalpha, which means it's probably a string, or join the object with a colon otherwise. Alternatively (thanks, @Padraic), you can use ':'.join(k) if isinstance(k, tuple) else k.

TigerhawkT3
  • 48,464
  • 6
  • 60
  • 97
1

You only care about dicts and tuples so just check for both recursing on the values:

def rec(d):
    for k,v in d.items():
        if isinstance(v, dict):
            rec(v)
        if isinstance(k, tuple):
            del d[k]
            d[":".join(k)] = v

rec(data)

from pprint import pprint as pp
pp(data)

Output:

{'category1:category2': {'numeric_float1': {'Green:Car': 0.5137635456103902,
                                            'Red:Plane': 0.42304110216698415,
                                            'Yellow:Boat': 0.5679229894797324}}}

This modifies the original dict which I presumed was the actual goal.

If you wanted it to work for all iterables except a str:

from collections import Iterable
def rec(d):
    for k, v in d.items():
        if isinstance(v, dict):
            rec(v)
        if isinstance(k, Iterable) and not isinstance(k,  str):
            del d[k]
            d[":".join(k)] = v
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321
1

Inspired by @TigerhawkT3's answer, here's somewhat of a "quack-listener":

[':'.join(k), k][k in k]

You can use that instead of your unconditional ':'.join(k). Other ideas:

[':'.join(k), k][''.join(k) == k]
[':'.join(k), k][str(k) == k]

I should say that these are confusing and do unnecessary work, though. This is just for fun/golfing. The ... if isinstance(...) else ... is the proper way. Although, k in k might actually be faster than isinstance(k, str):

>>> timeit('k in k',             "k = 'numeric_float1'")
0.222242249806186
>>> timeit('isinstance(k, str)', "k = 'numeric_float1'")
0.3160444680784167

>>> timeit('k in k',             "k = ('Yellow', 'Boat')")
0.21133306092963267
>>> timeit('isinstance(k, str)', "k = ('Yellow', 'Boat')")
0.5903861610393051
Community
  • 1
  • 1
Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107