web-dev-qa-db-fra.com

Aplatir les dictionnaires imbriqués, compresser les clés

Supposons que vous ayez un dictionnaire comme:

{'a': 1,
 'c': {'a': 2,
       'b': {'x': 5,
             'y' : 10}},
 'd': [1, 2, 3]}

Comment voulez-vous résoudre le problème en quelque chose comme:

{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}
135
A Timmes

Essentiellement, de la même manière que vous réduisez une liste imbriquée, vous devez simplement effectuer un travail supplémentaire pour itérer le dict par clé/valeur, créer de nouvelles clés pour votre nouveau dictionnaire et créer le dictionnaire à l'étape finale.

import collections

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
184
Imran

L'affiche originale doit tenir compte de deux grandes considérations:

  1. Existe-t-il des problèmes de chevauchement de l'espace de clé? Par exemple, {'a_b':{'c':1}, 'a':{'b_c':2}} Donnerait {'a_b_c':???}. La solution ci-dessous élude le problème en renvoyant un nombre itérable de paires.
  2. Si les performances posent problème, la fonction key-reducer (que je qualifie ici de 'join') nécessite un accès à l'intégralité du chemin clé, ou peut-elle simplement faire O(1) Si vous voulez pouvoir dire joinedKey = '_'.join(*keys), cela vous coûtera O (N ^ 2) temps d'exécution, mais si vous êtes prêt à dire nextKey = previousKey+'_'+thisKey, cela vous donne O(N) temps. La solution ci-dessous vous permet de faire les deux (puisque vous pouvez simplement concaténer toutes les clés, puis les post-traiter).

(Les performances ne sont probablement pas un problème, mais je développerai le deuxième point au cas où quelqu'un d’autre serait intéressé: il existe de nombreux choix dangereux dans la mise en œuvre. Si vous le faites de manière récursive et que vous cédez et redonnez, ou n'importe quoi équivalent qui touche plusieurs fois les nœuds (ce qui est assez facile à faire accidentellement), vous effectuez potentiellement un travail O (N ^ 2) plutôt que O (N). C’est peut-être parce que vous calculez une clé a alors a_1 Puis a_1_i ..., puis vous calculez a puis a_1 Puis a_1_ii ..., mais vous ne devriez vraiment pas avoir à calculer à nouveau a_1. Même si vous ne le recalculez pas, le renvoyer (une approche "niveau par niveau") est tout aussi mauvais. Un bon exemple est de penser à la performance sur {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Ci-dessous, une fonction que j’ai écrite flattenDict(d, join=..., lift=...), qui peut être adaptée à de nombreuses fins et peut faire ce que vous voulez. Malheureusement, il est assez difficile de faire une version paresseuse de cette fonction sans encourir les pénalités de performance ci-dessus (beaucoup de python comme trois versions différentes de ce code avant de choisir celui-ci).

from collections import Mapping
from itertools import chain
from operator import add

_FLAG_FIRST = object()

def flattenDict(d, join=add, lift=lambda x:x):
    results = []
    def visit(subdict, results, partialKey):
        for k,v in subdict.items():
            newKey = lift(k) if partialKey==_FLAG_FIRST else join(partialKey,lift(k))
            if isinstance(v,Mapping):
                visit(v, results, newKey)
            else:
                results.append((newKey,v))
    visit(d, results, _FLAG_FIRST)
    return results

Pour mieux comprendre ce qui se passe, vous trouverez ci-dessous un diagramme pour ceux qui ne connaissent pas reduce (à gauche), aussi connu sous le nom de "pli à gauche". Parfois, il est dessiné avec une valeur initiale à la place de k0 (ne faisant pas partie de la liste, passée dans la fonction). Ici, J est notre fonction join. Nous pré-traitons chaque kn avec lift(k).

               [k0,k1,...,kN].foldleft(J)
                           /    \
                         ...    kN
                         /
       J(k0,J(k1,J(k2,k3)))
                       /  \
                      /    \
           J(J(k0,k1),k2)   k3
                    /   \
                   /     \
             J(k0,k1)    k2
                 /  \
                /    \
               k0     k1

C'est en fait la même chose que functools.reduce, Mais notre fonction le fait pour tous les chemins de clé de l'arbre.

>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)

Démonstration (que je mettrais sinon dans docstring):

>>> testData = {
        'a':1,
        'b':2,
        'c':{
            'aa':11,
            'bb':22,
            'cc':{
                'aaa':111
            }
        }
    }
from pprint import pprint as pp

>>> pp(dict( flattenDict(testData, lift=lambda x:(x,)) ))
{('a',): 1,
 ('b',): 2,
 ('c', 'aa'): 11,
 ('c', 'bb'): 22,
 ('c', 'cc', 'aaa'): 111}

>>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b) ))
{'a': 1, 'b': 2, 'c_aa': 11, 'c_bb': 22, 'c_cc_aaa': 111}    

>>> pp(dict( (v,k) for k,v in flattenDict(testData, lift=hash, join=lambda a,b:hash((a,b))) ))
{1: 12416037344,
 2: 12544037731,
 11: 5470935132935744593,
 22: 4885734186131977315,
 111: 3461911260025554326}

Performance:

from functools import reduce
def makeEvilDict(n):
    return reduce(lambda acc,x:{x:acc}, [{i:0 for i in range(n)}]+range(n))

import timeit
def time(runnable):
    t0 = timeit.default_timer()
    _ = runnable()
    t1 = timeit.default_timer()
    print('took {:.2f} seconds'.format(t1-t0))

>>> pp(makeEvilDict(8))
{7: {6: {5: {4: {3: {2: {1: {0: {0: 0,
                                 1: 0,
                                 2: 0,
                                 3: 0,
                                 4: 0,
                                 5: 0,
                                 6: 0,
                                 7: 0}}}}}}}}}

import sys
sys.setrecursionlimit(1000000)

forget = lambda a,b:''

>>> time(lambda: dict(flattenDict(makeEvilDict(10000), join=forget)) )
took 0.10 seconds
>>> time(lambda: dict(flattenDict(makeEvilDict(100000), join=forget)) )
[1]    12569 segmentation fault  python

... soupir, ne pense pas que c'est de ma faute ...


[note historique sans importance en raison de problèmes de modération]

Concernant le duplicata présumé de Aplatir un dictionnaire de dictionnaires (2 niveaux de profondeur) de listes en Python :

La solution de cette question peut être implémentée par rapport à celle-ci en faisant sorted( sum(flatten(...),[]) ). L’inverse n’est pas possible: alors qu’il est vrai que les valeurs de flatten(...) peuvent être récupérées du duplicata présumé en mappant un -accumulateur, on ne peut pas récupérer les clés. (edit: il s'avère également que la question du prétendu propriétaire de duplicata est complètement différente, en ce sens qu'elle ne traite que des dictionnaires avec une profondeur de 2 niveaux exactement, bien qu'une des réponses sur cette page donne une solution générale.)

58
ninjagecko

Ou si vous utilisez déjà des pandas, vous pouvez le faire avec json_normalize() comme suit:

import pandas as pd

d = {'a': 1,
     'c': {'a': 2, 'b': {'x': 5, 'y' : 10}},
     'd': [1, 2, 3]}

df = pd.io.json.json_normalize(d, sep='_')

print(df.to_dict(orient='records')[0])

Sortie:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
34
MYGz

Voici une sorte d'implémentation "fonctionnelle", "one-liner". Il est récursif et basé sur une expression conditionnelle et une compréhension dictée.

def flatten_dict(dd, separator='_', prefix=''):
    return { prefix + separator + k if prefix else k : v
             for kk, vv in dd.items()
             for k, v in flatten_dict(vv, separator, kk).items()
             } if isinstance(dd, dict) else { prefix : dd }

Tester:

In [2]: flatten_dict({'abc':123, 'hgf':{'gh':432, 'yu':433}, 'gfd':902, 'xzxzxz':{"432":{'0b0b0b':231}, "43234":1321}}, '.')
Out[2]: 
{'abc': 123,
 'gfd': 902,
 'hgf.gh': 432,
 'hgf.yu': 433,
 'xzxzxz.432.0b0b0b': 231,
 'xzxzxz.43234': 1321}
23
dividebyzero

Si vous utilisez pandas, une fonction est cachée dans pandas.io.json.normalize appelé nested_to_record qui fait cela exactement.

from pandas.io.json.normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')
12
Aaron N. Brock

Code:

test = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}

def parse_dict(init, lkey=''):
    ret = {}
    for rkey,val in init.items():
        key = lkey+rkey
        if isinstance(val, dict):
            ret.update(parse_dict(val, key+'_'))
        else:
            ret[key] = val
    return ret

print(parse_dict(test,''))

Résultats:

$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

J'utilise python3.2, mise à jour pour votre version de python.

12

Cela ne se limite pas aux dictionnaires, mais à tous les types de mappage qui implémentent .items (). En outre, il est plus rapide car il évite une condition if. Néanmoins, les crédits vont à Imran:

def flatten(d, parent_key=''):
    items = []
    for k, v in d.items():
        try:
            items.extend(flatten(v, '%s%s_' % (parent_key, k)).items())
        except AttributeError:
            items.append(('%s%s' % (parent_key, k), v))
    return dict(items)
6

Mon Python 3.3 Solution utilisant des générateurs:

def flattenit(pyobj, keystring=''):
   if type(pyobj) is dict:
     if (type(pyobj) is dict):
         keystring = keystring + "_" if keystring else keystring
         for k in pyobj:
             yield from flattenit(pyobj[k], keystring + k)
     Elif (type(pyobj) is list):
         for lelm in pyobj:
             yield from flatten(lelm, keystring)
   else:
      yield keystring, pyobj

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

#your flattened dictionary object
flattened={k:v for k,v in flattenit(my_obj)}
print(flattened)

# result: {'c_b_y': 10, 'd': [1, 2, 3], 'c_a': 2, 'a': 1, 'c_b_x': 5}
4
Atul

Fonction simple pour aplatir les dictionnaires imbriqués. Pour Python 3, remplacez .iteritems() par .items()

def flatten_dict(init_dict):
    res_dict = {}
    if type(init_dict) is not dict:
        return res_dict

    for k, v in init_dict.iteritems():
        if type(v) == dict:
            res_dict.update(flatten_dict(v))
        else:
            res_dict[k] = v

    return res_dict

L'idée/exigence était la suivante: obtenir des dictionnaires à plat sans conserver les clés parent.

Exemple d'utilisation:

dd = {'a': 3, 
      'b': {'c': 4, 'd': 5}, 
      'e': {'f': 
                 {'g': 1, 'h': 2}
           }, 
      'i': 9,
     }

flatten_dict(dd)

>> {'a': 3, 'c': 4, 'd': 5, 'g': 1, 'h': 2, 'i': 9}

Garder les clés parentales est également simple.

4
Ivy Growing

Que diriez-vous d'une solution fonctionnelle et performante en Python3.5?

from functools import reduce


def _reducer(items, key, val, pref):
    if isinstance(val, dict):
        return {**items, **flatten(val, pref + key)}
    else:
        return {**items, pref + key: val}

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: _reducer(new_d, *kv, pref), 
        d.items(), 
        {}
    ))

C'est encore plus performant:

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: \
            isinstance(kv[1], dict) and \
            {**new_d, **flatten(kv[1], pref + kv[0])} or \
            {**new_d, pref + kv[0]: kv[1]}, 
        d.items(), 
        {}
    ))

Utilisé:

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

print(flatten(my_obj)) 
# {'d': [1, 2, 3], 'cby': 10, 'cbx': 5, 'ca': 2, 'a': 1}
4
Rotareti

Ceci est similaire à la réponse d'imran et de ralu. Il n'utilise pas de générateur, mais utilise la récursivité avec une fermeture:

def flatten_dict(d, separator='_'):
  final = {}
  def _flatten_dict(obj, parent_keys=[]):
    for k, v in obj.iteritems():
      if isinstance(v, dict):
        _flatten_dict(v, parent_keys + [k])
      else:
        key = separator.join(parent_keys + [k])
        final[key] = v
  _flatten_dict(d)
  return final

>>> print flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
3
Jonathan Drake

La solution de Davoud est très gentille mais ne donne pas de résultats satisfaisants lorsque le dict imbriqué contient également des listes de dict, mais que son code soit adapté à ce cas:

def flatten_dict(d):
    items = []
    for k, v in d.items():
        try:
            if (type(v)==type([])): 
                for l in v: items.extend(flatten_dict(l).items())
            else: 
                items.extend(flatten_dict(v).items())
        except AttributeError:
            items.append((k, v))
    return dict(items)
3
user3830731
def flatten(unflattened_dict, separator='_'):
    flattened_dict = {}

    for k, v in unflattened_dict.items():
        if isinstance(v, dict):
            sub_flattened_dict = flatten(v, separator)
            for k2, v2 in sub_flattened_dict.items():
                flattened_dict[k + separator + k2] = v2
        else:
            flattened_dict[k] = v

    return flattened_dict
2
Pari Rajaram

Si vous voulez mettre à plat le dictionnaire imbriqué et que vous voulez une liste de clés uniques, alors voici la solution:

def flat_dict_return_unique_key(data, unique_keys=set()):
    if isinstance(data, dict):
        [unique_keys.add(i) for i in data.keys()]
        for each_v in data.values():
            if isinstance(each_v, dict):
                flat_dict_return_unique_key(each_v, unique_keys)
    return list(set(unique_keys))
2
Ranvijay Sachan

Les réponses ci-dessus fonctionnent vraiment bien. Je pensais juste que j'ajouterais la fonction unflatten que j'ai écrite:

def unflatten(d):
    ud = {}
    for k, v in d.items():
        context = ud
        for sub_key in k.split('_')[:-1]:
            if sub_key not in context:
                context[sub_key] = {}
            context = context[sub_key]
        context[k.split('_')[-1]] = v
    return ud

Remarque: cela ne représente pas le '_' déjà présent dans les clés, un peu comme les contreparties aplaties.

2
tarequeh

Voici un algorithme pour un remplacement élégant et en place. Testé avec Python 2.7 et Python 3.5. Utilisation du caractère à points comme séparateur.

def flatten_json(json):
    if type(json) == dict:
        for k, v in list(json.items()):
            if type(v) == dict:
                flatten_json(v)
                json.pop(k)
                for k2, v2 in v.items():
                    json[k+"."+k2] = v2

Exemple:

d = {'a': {'b': 'c'}}                   
flatten_json(d)
print(d)
unflatten_json(d)
print(d)

Sortie:

{'a.b': 'c'}
{'a': {'b': 'c'}}

J'ai publié ce code ici avec le correspondant unflatten_json une fonction.

2
Alexander Ryzhov

Utilisation de générateurs:

def flat_dic_helper(prepand,d):
    if len(prepand) > 0:
        prepand = prepand + "_"
    for k in d:
        i=d[k]
        if type(i).__name__=='dict':
            r = flat_dic_helper(prepand+k,i)
            for j in r:
                yield j
        else:
            yield (prepand+k,i)

def flat_dic(d): return dict(flat_dic_helper("",d))

d={'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
print(flat_dic(d))


>> {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}
1
Luka Rahne

Utilisation de dict.popitem () dans une récursion directe semblable à une liste imbriquée:

def flatten(d):
    if d == {}:
        return d
    else:
        k,v = d.popitem()
        if (dict != type(v)):
            return {k:v, **flatten(d)}
        else:
            flat_kv = flatten(v)
            for k1 in list(flat_kv.keys()):
                flat_kv[k + '_' + k1] = flat_kv[k1]
                del flat_kv[k1]
            return {**flat_kv, **flatten(d)}
1
FredAKA

Je préfère toujours accéder aux objets dict via .items(), aussi, pour aplatir les dictures, j'utilise le générateur récursif suivant flat_items(d). Si vous aimez avoir à nouveau dict, enveloppez-le simplement comme ceci: flat = dict(flat_items(d))

def flat_items(d, key_separator='.'):
    """
    Flattens the dictionary containing other dictionaries like here: https://stackoverflow.com/questions/6027558/flatten-nested-python-dictionaries-compressing-keys

    >>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
    >>> flat = dict(flat_items(example, key_separator='_'))
    >>> assert flat['c_b_y'] == 10
    """
    for k, v in d.items():
        if type(v) is dict:
            for k1, v1 in flat_items(v, key_separator=key_separator):
                yield key_separator.join((k, k1)), v1
        else:
            yield k, v
0
Vladimir Ignatyev