52 votes

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

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

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 sous la forme

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é. Je veux que cela fonctionne pour des dictionnaires de 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 pratiquement aucune pour imbriquer un dictionnaire aplati. Les valeurs stockées dans le dictionnaire sont soit des scalaires, soit des chaînes de caractères, jamais des itérables.

Jusqu'à présent, j'ai obtenu 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 le moyen d'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 ça : 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 mettre des dictionnaires plats comme attributs, donc je veux déflater le dictionnaire que j'ai précédemment stocké dans le fichier NetCDF).

9 votes

Question bien écrite.

28voto

jdehesa Points 22254

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

0 votes

C'est la solution qui se rapproche le plus de ce que j'avais imaginé, merci !

0 votes

J'aime cet algorithme récursif astucieux !

24voto

cwallenpoole Points 34940
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

3 votes

Un peu plus lisible, à mon avis, est de décompresser en ligne : *initial_keys, final_key = k.split('_') . Mais excellente réponse !

0 votes

C'est vraiment bien, j'aime comment il n'utilise pas la récursion.

15voto

jpp Points 83462

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

  1. Créez un fichier imbriqué defaultdict de defaultdict objets.
  2. Interroger les éléments dans flat dictionnaire d'entrée.
  3. Construire defaultdict résultat selon la structure dérivée du fractionnement des clés par _ en utilisant getFromDict pour itérer le dictionnaire de résultats.

C'est 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}}}

En dernier lieu, vous pouvez convertir votre defaultdict à un régulier dict mais cette étape n'est 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)

0 votes

*keys, final_key = ... - Quelle est cette sorcellerie ? :O Au fait, +1.

2 votes

@DavidFoerster, Cela décompose la liste générée par k.split('_') en une liste et une chaîne de caractères, où la chaîne de caractères est la séparation finale. Cela supprime la nécessité d'une indexation positionnelle ultérieure.

1 votes

C'est ce que je pourrais laisser entendre, mais je n'étais absolument pas au courant de cette caractéristique linguistique.

4voto

Hans Musgrave Points 3316

Les autres réponses sont plus propres, mais puisque vous avez mentionné la récursion, 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 _}

4voto

Ajax1234 Points 42210

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 votes

J'aime l'utilisation de groupby !, mais j'ai peur que cela soit beaucoup moins lisible que d'autres solutions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X