web-dev-qa-db-fra.com

Création d'un dictionnaire imbriqué à partir d'un dictionnaire aplati

J'ai un dictionnaire aplati que je veux transformer en dictionnaire imbriqué, de la forme

flat = {'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}

Je veux le convertir dans le formulaire

nested = {'X': {'a': {'one': 10,
                      'two': 20}, 
                'b': {'one': 10,
                      'two': 20}}, 
          'Y': {'a': {'one': 10,
                      'two': 20},
                'b': {'one': 10,
                      'two': 20}}}

La structure du dictionnaire plat est telle qu'il ne devrait pas y avoir de problèmes d'ambiguïtés. Je veux que cela fonctionne pour les dictionnaires d'une profondeur arbitraire, mais les performances ne sont pas vraiment un problème. J'ai vu beaucoup de méthodes pour aplatir un dictionnaire imbriqué, mais essentiellement aucune pour imbriquer un dictionnaire aplati. Les valeurs stockées dans le dictionnaire sont des scalaires ou des chaînes, jamais itérables.

Jusqu'à présent, j'ai quelque chose qui peut prendre l'entrée

test_dict = {'X_a_one': '10',
             'X_b_one': '10',
             'X_c_one': '10'}

à la sortie

test_out = {'X': {'a_one': '10', 
                  'b_one': '10', 
                  'c_one': '10'}}

en utilisant le code

def nest_once(inp_dict):
    out = {}
    if isinstance(inp_dict, dict):
        for key, val in inp_dict.items():
            if '_' in key:
                head, tail = key.split('_', 1)

                if head not in out.keys():
                    out[head] = {tail: val}
                else:
                    out[head].update({tail: val})
            else:
                out[key] = val
    return out

test_out = nest_once(test_dict)

Mais j'ai du mal à trouver comment en faire quelque chose qui crée récursivement tous les niveaux du dictionnaire.

Toute aide serait appréciée!

(Quant à savoir pourquoi je veux faire ceci: j'ai un fichier dont la structure est équivalente à un dict imbriqué, et je veux stocker le contenu de ce fichier dans le dictionnaire d'attributs d'un fichier NetCDF et le récupérer plus tard. Cependant NetCDF ne vous permet que de mettez des dictionnaires plats comme attributs, donc je veux aplatir le dictionnaire que j'ai précédemment stocké dans le fichier NetCDF.)

50
ThomasNicholas

Voici mon point de vue:

def nest_dict(flat):
    result = {}
    for k, v in flat.items():
        _nest_dict_rec(k, v, result)
    return result

def _nest_dict_rec(k, v, out):
    k, *rest = k.split('_', 1)
    if rest:
        _nest_dict_rec(rest[0], v, out.setdefault(k, {}))
    else:
        out[k] = v

flat = {'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}
nested = {'X': {'a': {'one': 10,
                      'two': 20}, 
                'b': {'one': 10,
                      'two': 20}}, 
          'Y': {'a': {'one': 10,
                      'two': 20},
                'b': {'one': 10,
                      'two': 20}}}
print(nest_dict(flat) == nested)
# True
25
jdehesa
output = {}

for k, v in source.items():
    # always start at the root.
    current = output

    # This is the part you're struggling with.
    pieces = k.split('_')

    # iterate from the beginning until the second to last place
    for piece in pieces[:-1]:
       if not piece in current:
          # if a dict doesn't exist at an index, then create one
          current[piece] = {}

       # as you walk into the structure, update your current location
       current = current[piece]

    # The reason you're using the second to last is because the last place
    # represents the place you're actually storing the item
    current[pieces[-1]] = v
24
cwallenpoole

Voici une façon d'utiliser collections.defaultdict, empruntant fortement à cette réponse précédente . Il y a 3 étapes:

  1. Créez un defaultdict imbriqué d'objets defaultdict.
  2. Itérer les éléments dans le dictionnaire d'entrée flat.
  3. Générez le résultat defaultdict selon la structure dérivée de la division des clés par _, en utilisant getFromDict pour itérer le dictionnaire de résultats.

Voici un exemple complet:

from collections import defaultdict
from functools import reduce
from operator import getitem

def getFromDict(dataDict, mapList):
    """Iterate nested dictionary"""
    return reduce(getitem, mapList, dataDict)

# instantiate nested defaultdict of defaultdicts
tree = lambda: defaultdict(tree)
d = tree()

# iterate input dictionary
for k, v in flat.items():
    *keys, final_key = k.split('_')
    getFromDict(d, keys)[final_key] = v

{'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}},
 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

Comme dernière étape, vous pouvez convertir votre defaultdict en un dict normal, bien que cette étape ne soit généralement pas nécessaire.

def default_to_regular_dict(d):
    """Convert nested defaultdict to regular dict of dicts."""
    if isinstance(d, defaultdict):
        d = {k: default_to_regular_dict(v) for k, v in d.items()}
    return d

# convert back to regular dict
res = default_to_regular_dict(d)
13
jpp

Les autres réponses sont plus claires, mais comme vous avez mentionné la récursivité, nous avons d'autres options.

def nest(d):
    _ = {}
    for k in d:
        i = k.find('_')
        if i == -1:
            _[k] = d[k]
            continue
        s, t = k[:i], k[i+1:]
        if s in _:
            _[s][t] = d[k]
        else:
            _[s] = {t:d[k]}
    return {k:(nest(_[k]) if type(_[k])==type(d) else _[k]) for k in _}
4
Hans Musgrave

Une autre solution non récursive sans importation. Fractionner la logique entre l'insertion de chaque paire clé-valeur du dict plat et le mappage sur les paires clé-valeur du dict plat.

def insert(dct, lst):
    """
    dct: a dict to be modified inplace.
    lst: list of elements representing a hierarchy of keys
    followed by a value.

    dct = {}
    lst = [1, 2, 3]

    resulting value of dct: {1: {2: 3}}
    """
    for x in lst[:-2]:
        dct[x] = dct = dct.get(x, dict())

    dct.update({lst[-2]: lst[-1]})


def unflat(dct):
    # empty dict to store the result
    result = dict()

    # create an iterator of lists representing hierarchical indices followed by the value
    lsts = ([*k.split("_"), v] for k, v in dct.items())

    # insert each list into the result
    for lst in lsts:
        insert(result, lst)

    return result


result = unflat(flat)
# {'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}},
# 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

Vous pouvez utiliser itertools.groupby:

import itertools, json
flat = {'Y_a_two': 20, 'Y_a_one': 10, 'X_b_two': 20, 'X_b_one': 10, 'X_a_one': 10, 'X_a_two': 20, 'Y_b_two': 20, 'Y_b_one': 10}
_flat = [[*a.split('_'), b] for a, b in flat.items()]
def create_dict(d): 
  _d = {a:list(b) for a, b in itertools.groupby(sorted(d, key=lambda x:x[0]), key=lambda x:x[0])}
  return {a:create_dict([i[1:] for i in b]) if len(b) > 1 else b[0][-1] for a, b in _d.items()}

print(json.dumps(create_dict(_flat), indent=3))

Sortie:

{
 "Y": {
    "b": {
      "two": 20,
      "one": 10
    },
    "a": {
      "two": 20,
      "one": 10
    }
 },
  "X": {
     "b": {
     "two": 20,
     "one": 10
   },
    "a": {
     "two": 20,
     "one": 10
   }
 }
}
4
Ajax1234

Voici un résultat récursif raisonnablement lisible:

def unflatten_dict(a, result=None, sep='_'):

    if result is None:
        result = dict()

    for k, v in a.items():
        k, *rest = k.split(sep, 1)
        if rest:
            unflatten_dict({rest[0]: v}, result.setdefault(k, {}), sep=sep)
        else:
            result[k] = v

    return result


flat = {'X_a_one': 10,
        'X_a_two': 20,
        'X_b_one': 10,
        'X_b_two': 20,
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}

print(unflatten_dict(flat))
{'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}, 
 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

Ceci est basé sur quelques-unes des réponses ci-dessus, n'utilise aucune importation et n'est testé que dans python 3.

1
makeyourownmaker