Aplatir les dictionnaires imbriqués, compression des clés

173

Supposons que vous ayez un dictionnaire comme:

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

Comment pourriez-vous l'aplatir en quelque chose comme:

{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}
A Timmes
la source
2
aussi, il y a une bibliothèque pour cela: github.com/ianlini/flatten-dict
Ufos
voir aussi: stackoverflow.com/questions/14692690
dreftymac

Réponses:

221

En gros, de la même façon que vous aplatiriez une liste imbriquée, il vous suffit de faire le 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}
Imran
la source
7
Si vous remplacez le isinstancepar un try..exceptbloc, cela fonctionnera pour tout mappage, même s'il n'est pas dérivé de dict.
Björn Pollex
1
Je l'ai changé pour tester pour collections.MutableMappingle rendre plus générique. Mais pour Python <2.6, try..exceptc'est probablement la meilleure option.
Imran
5
Si vous voulez que les dictionnaires vides soient conservés dans une version aplatie, vous voudrez peut-être changer if isinstance(v, collections.MutableMapping):pourif v and isinstance(v, collections.MutableMapping):
tarequeh
3
Notez que cela new_key = parent_key + sep + k if parent_key else ksuppose que les clés sont toujours des chaînes, sinon cela augmentera TypeError: cannot concatenate 'str' and [other] objects. Cependant, vous pouvez résoudre ce problème en contraignant simplement kstring ( str(k)), ou en concaténant des clés dans un tuple au lieu d'une chaîne (les tuples peuvent également être des clés dict).
Scott H
1
Et la fonction de gonflage est
mitch
66

L'affiche originale doit prendre en compte deux grandes considérations:

  1. Y a-t-il des problèmes de clobber d'espace de clés? Par exemple, {'a_b':{'c':1}, 'a':{'b_c':2}}entraînerait {'a_b_c':???}. La solution ci-dessous élude le problème en renvoyant un itérable de paires.
  2. Si les performances sont un problème, la fonction de réduction de clé (que j'appelle ici `` rejoindre '') nécessite-t-elle un accès à l'ensemble du chemin de clé, ou peut-elle simplement faire fonctionner O (1) à chaque nœud de l'arborescence? Si vous voulez pouvoir dire joinedKey = '_'.join(*keys), cela vous coûtera O (N ^ 2) temps d'exécution. Cependant, si vous êtes prêt à le dire nextKey = previousKey+'_'+thisKey, cela vous donne du temps O (N). La solution ci-dessous vous permet de faire les deux (puisque vous pouvez simplement concaténer toutes les clés, puis les post-traiter).

( La performance est peu probable un problème, mais je vais élaborer sur le deuxième point au cas où quelqu'un d' autre soins. Dans la mise en œuvre cela, il y a de nombreux choix dangereux Si vous le faites de manière récursive et le rendement et re-rendement, ou quoi que ce soit équivalent qui touche nœuds plus d'une fois ( ce qui est assez facile à faire par hasard), vous êtes en train de faire potentiellement O (N ^ 2) travail plutôt que O (N). en effet , vous êtes peut - être une clé calcules apuis a_1alors a_1_i..., puis le calcul aalors a_1alors a_1_ii..., mais en réalité, vous ne devriez pas avoir à calculer à a_1nouveau. Même si vous ne le recalculez pas, le redonner (une approche "niveau par niveau") est tout aussi mauvais. Un bon exemple est penser à la performance sur {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Voici une fonction que j'ai écrite flattenDict(d, join=..., lift=...)qui peut être adaptée à de nombreuses fins et qui peut faire ce que vous voulez. Malheureusement, il est assez difficile de créer une version paresseuse de cette fonction sans encourir les pénalités de performances ci-dessus (de nombreuses versions intégrées de python comme chain.from_iterable ne sont pas réellement efficaces, ce que je n'ai réalisé qu'après des tests approfondis de trois versions différentes de ce code avant de me décider celui-là).

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), autrement connu sous le nom de «repli à gauche». Parfois, il est dessiné avec une valeur initiale à la place de k0 (ne fait pas partie de la liste, passé dans la fonction). Voici Jnotre joinfonction. Nous prétraitons chaque k n 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 où notre fonction le fait à 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 autrement 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 prétendu duplicata de Flatten un dictionnaire de dictionnaires (2 niveaux de profondeur) de listes en Python :

La solution de cette question peut être mise en œuvre en fonction de celle-ci en faisant sorted( sum(flatten(...),[]) ). L'inverse n'est pas possible: s'il est vrai que les valeurs de flatten(...)peuvent être récupérées à partir du prétendu doublon en mappant un accumulateur d'ordre supérieur, on ne peut pas récupérer les clés. (modifier: Il s'avère également que la question du prétendu propriétaire en double est complètement différente, en ce qu'elle ne traite que des dictionnaires exactement à 2 niveaux de profondeur, bien qu'une des réponses sur cette page donne une solution générale.)

ninjagecko
la source
2
Je ne sais pas si cela est pertinent par rapport à la question. Cette solution n'aplatit pas un élément de dictionnaire d'une liste de dictionnaires, c'est-à-dire {'a': [{'aa': 1}, {'ab': 2}]}. La fonction flattenDict peut être facilement modifiée pour s'adapter à ce cas.
Stewbaca
56

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

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])

Production:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
MYGz
la source
5
ou passez simplement l'argument de
Blue Moon
3
Dommage qu'il ne gère pas les listes :)
Roelant
32

Si vous utilisez, pandasil y a une fonction cachée dans pandas.io.json._normalize1 appelée nested_to_recordqui fait exactement cela.

from pandas.io.json._normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')

1 Dans les versions pandas 0.24.xet les anciennes utilisations pandas.io.json.normalize(sans le _)

Aaron N. Brock
la source
2
Ce qui a fonctionné pour moi était from pandas.io.json._normalize import nested_to_record. Notez le trait de soulignement ( _) avant normalize.
Eyal Levin
3
@EyalLevin Bonne prise! Cela a changé 0.25.x, j'ai mis à jour la réponse. :)
Aaron
29

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

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}
diviser par zéro
la source
1
Cela ne fonctionne pas pour les dictionnaires généraux, en particulier, avec des clés de tuple, par exemple, remplacez ('hgf',2)la 2ème clé dans vos lancers de testTypeError
alancalvitti
1
@alancalvitti Cela suppose qu'il s'agit d'une chaîne ou de quelque chose d'autre qui prend en charge l' +opérateur. Pour toute autre chose, vous devrez vous adapter prefix + separator + kà l'appel de fonction approprié pour composer les objets.
dividebyzero
1
Un autre problème concernant les clés de tuple. J'ai posté séparément comment généraliser en fonction de votre méthode. Cependant, il ne peut pas gérer correctement l'exemple de ninjageko:{'a_b':{'c':1}, 'a':{'b_c':2}}
alancalvitti
3
Je devenais inquiet, ne voyant aucune réponse utilisant la récursivité. Quel est le problème avec nos jeunes ces jours-ci?
Jakov
1
ne fait rien si un dict a une liste imbriquée de dictés, comme ceci:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M
13

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.

Pavan Yalamanchili
la source
Vous souhaiterez probablement spécifier la valeur par défaut de lkey=''dans votre définition de fonction plutôt que lors de l'appel de la fonction. Voir d'autres réponses à ce sujet.
Acumenus
7

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}
Rotareti
la source
2
Que diriez-vous d'une solution lisible et fonctionnelle? ;) Sur quelle version avez-vous testé cela? J'obtiens une "erreur de syntaxe" en essayant ceci dans Python 3.4.3. Il semble que l'utilisation de "** all" n'est pas légitime.
Ingo Fischer
Je travaille depuis Python 3.5. Je ne savais pas que cela ne fonctionnait pas avec 3.4. Vous avez raison, ce n'est pas très lisible. J'ai mis à jour la réponse. J'espère que c'est plus lisible maintenant. :)
Rotareti
1
Ajout de l'importation de réduction manquante. Je trouve toujours le code difficile à comprendre et je pense que c'est un bon exemple de la raison pour laquelle Guido van Rossum lui-même a découragé l'utilisation de lambda, réduire, filtrer et cartographier déjà en 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer
Je suis d'accord. Python n'est pas vraiment conçu pour la programmation fonctionnelle . Pourtant, je pense que reducec'est génial au cas où vous auriez besoin de réduire les dictionnaires. J'ai mis à jour la réponse. Devrait avoir l'air un peu plus pythonique maintenant.
Rotareti
7

Cela n'est pas limité aux dictionnaires, mais à tous les types de mappage qui implémentent .items (). De plus, c'est plus rapide car cela é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)
Davoud Taghawi-Nejad
la source
2
Si ce dn'est pas un dicttype de mappage personnalisé mais qui n'implémente pas items, votre fonction échouerait immédiatement. Donc, cela ne fonctionne pas pour tous les types de mappage mais seulement ceux qui implémentent items().
user6037143
1
@ user6037143 avez-vous déjà rencontré un type de mappage qui ne s'implémente pas items? Je serais curieux d'en voir un.
Trey Hunner
2
@ user6037143, non vous ne l'avez pas par définition si les éléments ne sont pas implémentés, ce n'est pas un type de mappage.
Davoud Taghawi-Nejad
1
@ DavoudTaghawi-Nejad, pourriez-vous modifier ceci pour gérer les clés générales, par exemple les tuples qui ne devraient pas être aplatis en interne.
alancalvitti
6

Ma solution Python 3.3 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}
Atul
la source
pouvez-vous étendre pour gérer tout type de clé valide autre que str (y compris tuple)? Au lieu de la concaténation de chaînes, joignez-les dans un tuple.
alancalvitti
5

En utilisant la récursivité, en la gardant simple et lisible par l'homme:

def flatten_dict(dictionary, accumulator=None, parent_key=None, separator="."):
    if accumulator is None:
        accumulator = {}

    for k, v in dictionary.items():
        k = f"{parent_key}{separator}{k}" if parent_key else k
        if isinstance(v, dict):
            flatten_dict(dictionary=v, accumulator=accumulator, parent_key=k)
            continue

        accumulator[k] = v

    return accumulator

L'appel est simple:

new_dict = flatten_dict(dictionary)

ou

new_dict = flatten_dict(dictionary, separator="_")

si nous voulons changer le séparateur par défaut.

Une petite panne:

Lorsque la fonction est appelée pour la première fois, elle est appelée uniquement en passant le que dictionarynous voulons aplatir. Le accumulatorparamètre est là pour prendre en charge la récursivité, que nous verrons plus tard. Donc, nous instancions accumulatordans un dictionnaire vide où nous mettrons toutes les valeurs imbriquées de l'original dictionary.

if accumulator is None:
    accumulator = {}

Lorsque nous parcourons les valeurs du dictionnaire, nous construisons une clé pour chaque valeur. L' parent_keyargument sera Nonepour le premier appel, tandis que pour chaque dictionnaire imbriqué, il contiendra la clé pointant vers lui, donc nous ajoutons cette clé.

k = f"{parent_key}{separator}{k}" if parent_key else k

Dans le cas où la valeur vers laquelle pointe vla clé kest un dictionnaire, la fonction s'appelle elle-même, en passant le dictionnaire imbriqué, le accumulator(qui est passé par référence, donc toutes les modifications qui y sont apportées sont effectuées sur la même instance) et la clé kafin que nous peut construire la clé concaténée. Remarquez la continuedéclaration. Nous voulons sauter la ligne suivante, en dehors du ifbloc, afin que le dictionnaire imbriqué ne se retrouve pas dans la accumulatorclé under k.

if isinstance(v, dict):
    flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
    continue

Alors, que faisons-nous si la valeur vn'est pas un dictionnaire? Mettez-le simplement inchangé dans le fichier accumulator.

accumulator[k] = v

Une fois que nous avons terminé, nous retournons simplement le accumulator, en laissant l' dictionaryargument d' origine intact.

REMARQUE

Cela fonctionnera uniquement avec les dictionnaires qui ont des chaînes comme clés. Il fonctionnera avec des objets hachables implémentant la __repr__méthode, mais produira des résultats indésirables.

Jakov
la source
4

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: Obtenir des dictionnaires plats sans garder les clés parentes.

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}

La conservation des clés parentales est également simple.

Lierre poussant
la source
3

Ceci est similaire à la réponse d'imran et de ralu. Il n'utilise pas de générateur, mais utilise à la place 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}
Jonathan Drake
la source
Je ne suis pas sûr que l'utilisation du terme « fermeture » soit correcte ici, car la fonction _flatten_dictn'est jamais renvoyée et ne devrait jamais l'être. Elle peut peut-être être appelée à la place une sous - fonction ou une fonction incluse .
Acumenus
3

La solution de Davoud est très agréable mais ne donne pas de résultats satisfaisants lorsque le dict imbriqué contient également des listes de dictionnaires, mais son code doit être adapté pour 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)
user3830731
la source
Vous pouvez mettre en cache le résultat de type([])pour éviter un appel de fonction pour chaque élément du dict.
bfontaine
2
Veuillez utiliser à la isinstance(v, list)place
Druska
2

Les réponses ci-dessus fonctionnent vraiment bien. Je pensais juste que j'ajouterais la fonction non aplatie 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 tient pas compte de '_' déjà présent dans les clés, tout comme les homologues aplatis.

tarequeh
la source
2

Voici un algorithme pour un remplacement sur place élégant. Testé avec Python 2.7 et Python 3.5. Utilisation du caractère point 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)

Production:

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

J'ai publié ce code ici avec la unflatten_jsonfonction de correspondance .

Alexander Ryzhov
la source
2

Si vous souhaitez mettre à plat un dictionnaire imbriqué et que toutes les clés uniques sont répertoriées, 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))
Ranvijay Sachan
la source
2
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
Pari Rajaram
la source
2
def flatten_nested_dict(_dict, _str=''):
    '''
    recursive function to flatten a nested dictionary json
    '''
    ret_dict = {}
    for k, v in _dict.items():
        if isinstance(v, dict):
            ret_dict.update(flatten_nested_dict(v, _str = '_'.join([_str, k]).strip('_')))
        elif isinstance(v, list):
            for index, item in enumerate(v):
                if isinstance(item, dict):
                    ret_dict.update(flatten_nested_dict(item,  _str= '_'.join([_str, k, str(index)]).strip('_')))
                else:
                    ret_dict['_'.join([_str, k, str(index)]).strip('_')] = item
        else:
            ret_dict['_'.join([_str, k]).strip('_')] = v
    return ret_dict
Pradeep Pathak
la source
cela fonctionne avec les listes à l'intérieur de notre dict imbriqué, mais n'a pas d'option de séparateur personnalisé
Nikhil VJ
2

Je pensais à une sous-classe de UserDict pour aplatir automatiquement les touches.

class FlatDict(UserDict):
    def __init__(self, *args, separator='.', **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            for k1, v1 in FlatDict(value, separator=self.separator).items():
                super().__setitem__(f"{key}{self.separator}{k1}", v1)
        else:
            super().__setitem__(key, value)

‌ Les avantages que les clés peuvent être ajoutées à la volée, ou en utilisant l'instanciation de dict standard, sans surprise:

>>> fd = FlatDict(
...    {
...        'person': {
...            'sexe': 'male', 
...            'name': {
...                'first': 'jacques',
...                'last': 'dupond'
...            }
...        }
...    }
... )
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond'}
>>> fd['person'] = {'name': {'nickname': 'Bob'}}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob'}
>>> fd['person.name'] = {'civility': 'Dr'}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob', 'person.name.civility': 'Dr'}
loutre
la source
1
Assigner à fd ['person'] tout en conservant sa valeur existante est assez surprenant. Ce n'est pas ainsi que fonctionnent les dictionnaires réguliers.
tbm
1

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}
Luka Rahne
la source
2
type(i).__name__=='dict'pourrait être remplacé par type(i) is dictou peut-être même mieux isinstance(d, dict)(ou Mapping/ MutableMapping).
Cristian Ciupitu
1

Utilisation de dict.popitem () dans une récursivité simple de type 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)}
FredAKA
la source
1

Pas exactement ce que l'OP a demandé, mais beaucoup de gens viennent ici à la recherche de moyens d'aplatir les données JSON imbriquées du monde réel qui peuvent avoir des objets et des tableaux json à clé-valeur imbriqués et des objets json dans les tableaux, etc. JSON n'inclut pas les tuples, nous n'avons donc pas à nous en préoccuper.

J'ai trouvé une implémentation du commentaire d' inclusion de liste par @roneo à la réponse publiée par @Imran :

https://github.com/ScriptSmith/socialreaper/blob/master/socialreaper/tools.py#L8

import collections
def flatten(dictionary, parent_key=False, separator='.'):
    """
    Turn a nested dictionary into a flattened dictionary
    :param dictionary: The dictionary to flatten
    :param parent_key: The string to prepend to dictionary's keys
    :param separator: The string used to separate flattened keys
    :return: A flattened dictionary
    """

    items = []
    for key, value in dictionary.items():
        new_key = str(parent_key) + separator + key if parent_key else key
        if isinstance(value, collections.MutableMapping):
            items.extend(flatten(value, new_key, separator).items())
        elif isinstance(value, list):
            for k, v in enumerate(value):
                items.extend(flatten({str(k): v}, new_key).items())
        else:
            items.append((new_key, value))
    return dict(items)

Essaye-le:

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

>> {'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd.0': 1, 'd.1': 2, 'd.2': 3}

Et cela fait le travail dont j'ai besoin: je lance n'importe quel json compliqué et cela l'aplatit pour moi.

Tous les crédits à https://github.com/ScriptSmith .

Nikhil VJ
la source
1

En fait, j'ai récemment écrit un paquet appelé cherrypicker pour traiter exactement ce genre de chose puisque je devais le faire si souvent!

Je pense que le code suivant vous donnerait exactement ce que vous recherchez:

from cherrypicker import CherryPicker

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

picker = CherryPicker(dct)
picker.flatten().get()

Vous pouvez installer le package avec:

pip install cherrypicker

... et il y a plus de documents et de conseils sur https://cherrypicker.readthedocs.io .

D' autres méthodes peuvent être plus rapides, mais la priorité de ce paquet est de rendre ces tâches faciles . Si vous avez une longue liste d'objets à aplatir, vous pouvez également dire à CherryPicker d'utiliser le traitement parallèle pour accélérer les choses.

big-o
la source
J'aime l'approche alternative.
Gergely M
0

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

def flat_items(d, key_separator='.'):
    """
    Flattens the dictionary containing other dictionaries like here: /programming/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
Vladimir Ignatyev
la source
0

Variation de ce Dictionnaires imbriqués Flatten, compression des clés avec max_level et réducteur personnalisé.

  def flatten(d, max_level=None, reducer='tuple'):
      if reducer == 'tuple':
          reducer_seed = tuple()
          reducer_func = lambda x, y: (*x, y)
      else:
          raise ValueError(f'Unknown reducer: {reducer}')

      def impl(d, pref, level):
        return reduce(
            lambda new_d, kv:
                (max_level is None or level < max_level)
                and isinstance(kv[1], dict)
                and {**new_d, **impl(kv[1], reducer_func(pref, kv[0]), level + 1)}
                or {**new_d, reducer_func(pref, kv[0]): kv[1]},
                d.items(),
            {}
        )

      return impl(d, reducer_seed, 0)
user2528473
la source
0

Si les fonctions récursives ne vous dérangent pas, voici une solution. J'ai également pris la liberté d'inclure un paramètre d' exclusion au cas où vous souhaiteriez conserver une ou plusieurs valeurs.

Code:

def flatten_dict(dictionary, exclude = [], delimiter ='_'):
    flat_dict = dict()
    for key, value in dictionary.items():
        if isinstance(value, dict) and key not in exclude:
            flatten_value_dict = flatten_dict(value, exclude, delimiter)
            for k, v in flatten_value_dict.items():
                flat_dict[f"{key}{delimiter}{k}"] = v
        else:
            flat_dict[key] = value
    return flat_dict

Usage:

d = {'a':1, 'b':[1, 2], 'c':3, 'd':{'a':4, 'b':{'a':7, 'b':8}, 'c':6}, 'e':{'a':1,'b':2}}
flat_d = flatten_dict(dictionary=d, exclude=['e'], delimiter='.')
print(flat_d)

Production:

{'a': 1, 'b': [1, 2], 'c': 3, 'd.a': 4, 'd.b.a': 7, 'd.b.b': 8, 'd.c': 6, 'e': {'a': 1, 'b': 2}}
Thomas
la source
0

J'ai essayé certaines des solutions de cette page - mais pas toutes - mais celles que j'ai essayées n'ont pas réussi à gérer la liste imbriquée de dict.

Considérez un dict comme celui-ci:

d = {
        'owner': {
            'name': {'first_name': 'Steven', 'last_name': 'Smith'},
            'lottery_nums': [1, 2, 3, 'four', '11', None],
            'address': {},
            'tuple': (1, 2, 'three'),
            'tuple_with_dict': (1, 2, 'three', {'is_valid': False}),
            'set': {1, 2, 3, 4, 'five'},
            'children': [
                {'name': {'first_name': 'Jessica',
                          'last_name': 'Smith', },
                 'children': []
                 },
                {'name': {'first_name': 'George',
                          'last_name': 'Smith'},
                 'children': []
                 }
            ]
        }
    }

Voici ma solution improvisée:

def flatten_dict(input_node: dict, key_: str = '', output_dict: dict = {}):
    if isinstance(input_node, dict):
        for key, val in input_node.items():
            new_key = f"{key_}.{key}" if key_ else f"{key}"
            flatten_dict(val, new_key, output_dict)
    elif isinstance(input_node, list):
        for idx, item in enumerate(input_node):
            flatten_dict(item, f"{key_}.{idx}", output_dict)
    else:
        output_dict[key_] = input_node
    return output_dict

qui produit:

{
  owner.name.first_name: Steven,
  owner.name.last_name: Smith,
  owner.lottery_nums.0: 1,
  owner.lottery_nums.1: 2,
  owner.lottery_nums.2: 3,
  owner.lottery_nums.3: four,
  owner.lottery_nums.4: 11,
  owner.lottery_nums.5: None,
  owner.tuple: (1, 2, 'three'),
  owner.tuple_with_dict: (1, 2, 'three', {'is_valid': False}),
  owner.set: {1, 2, 3, 4, 'five'},
  owner.children.0.name.first_name: Jessica,
  owner.children.0.name.last_name: Smith,
  owner.children.1.name.first_name: George,
  owner.children.1.name.last_name: Smith,
}

Une solution de fortune et ce n'est pas parfait.
REMARQUE:

  • il ne garde pas de dictionnaires vides tels que la address: {}paire k / v.

  • il n'aplatira pas les dicts dans les tuples imbriqués - bien qu'il soit facile à ajouter en utilisant le fait que les tuples python agissent de la même manière que les listes.

Gergely M
la source
-1

Il suffit de l'utiliser python-benedict, c'est une sous-classe de dict qui offre de nombreuses fonctionnalités, dont une flattenméthode. Il est possible de l'installer en utilisant pip:pip install python-benedict

https://github.com/fabiocaccamo/python-benedict#flatten

from benedict import benedict 

d = benedict(data)
f = d.flatten(separator='_')
Fabio Caccamo
la source