Acoplar diccionarios anidados, comprimir claves

Respuestas:

221

Básicamente, de la misma manera que aplanaría una lista anidada, solo tiene que hacer el trabajo adicional para iterar el dict por clave / valor, crear nuevas claves para su nuevo diccionario y crear el diccionario en el paso final.

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
fuente
77
Si reemplaza el isinstancecon un try..exceptbloque, esto funcionará para cualquier asignación, incluso si no se deriva de dict.
Björn Pollex
1
Lo cambió para probarlo para collections.MutableMappinghacerlo más genérico. Pero para Python <2.6, try..exceptes probablemente la mejor opción.
Imran
55
Si quieres diccionarios vacíos que se conservan en la versión aplanada puede que desee cambiar if isinstance(v, collections.MutableMapping):aif v and isinstance(v, collections.MutableMapping):
tarequeh
3
Tenga en cuenta que se new_key = parent_key + sep + k if parent_key else ksupone que las teclas son siempre cadenas, de lo contrario se elevará TypeError: cannot concatenate 'str' and [other] objects. Sin embargo, puede solucionarlo simplemente coaccionando ka string ( str(k)), o concatenando claves en una tupla en lugar de una cadena (las tuplas también pueden ser claves dictadas).
Scott H
1
Y la función de inflado está aquí
mitch
66

Hay dos grandes consideraciones que el póster original debe tener en cuenta:

  1. ¿Hay problemas de bloqueo de espacio de teclas? Por ejemplo, {'a_b':{'c':1}, 'a':{'b_c':2}}resultaría en {'a_b_c':???}. La siguiente solución evade el problema devolviendo un iterable de pares.
  2. Si el rendimiento es un problema, ¿la función de reducción de clave (a la que me refiero como 'unir') requiere acceso a toda la ruta de la clave, o puede hacer que O (1) funcione en cada nodo del árbol? Si quieres poder decirlo joinedKey = '_'.join(*keys), eso te costará O (N ^ 2) tiempo de ejecución. Sin embargo, si estás dispuesto a decir nextKey = previousKey+'_'+thisKey, eso te da tiempo O (N). La solución a continuación le permite hacer ambas cosas (ya que podría simplemente concatenar todas las claves y luego procesarlas posteriormente).

(Es probable que el rendimiento no sea un problema, pero explicaré el segundo punto en caso de que a alguien más le importe: al implementar esto, hay numerosas opciones peligrosas. Si lo hace de forma recursiva y cede y vuelve a ceder, o cualquier cosa equivalente que toque nodos más de una vez (lo cual es bastante fácil de hacer accidentalmente), está haciendo un trabajo potencialmente O (N ^ 2) en lugar de O (N). Esto se debe a que tal vez está calculando una clave ay a_1luego a_1_i..., y luego calculando aentonces a_1entonces a_1_ii..., pero realmente no debería tener que calcular de a_1nuevo. Incluso si no lo está recalculando, volver a cederlo (un enfoque 'nivel por nivel') es igual de malo. Un buen ejemplo es pensar en el rendimiento en {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

A continuación hay una función que escribí flattenDict(d, join=..., lift=...)que se puede adaptar a muchos propósitos y puede hacer lo que quiera. Lamentablemente, es bastante difícil hacer una versión perezosa de esta función sin incurrir en las penalizaciones de rendimiento anteriores (muchas construcciones de python como chain.from_iterable no son realmente eficientes, de lo que solo me di cuenta después de probar exhaustivamente tres versiones diferentes de este código antes de establecerme éste).

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

Para comprender mejor lo que está sucediendo, a continuación hay un diagrama para aquellos que no están familiarizados con reduce(izquierda), también conocido como "doblar a la izquierda". A veces se dibuja con un valor inicial en lugar de k0 (no forma parte de la lista, se pasa a la función). Aquí, Jes nuestra joinfunción. Preprocesamos cada k n con 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

De hecho, esto es lo mismo que functools.reduce, pero nuestra función hace esto en todas las rutas de teclas del árbol.

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

Demostración (que de lo contrario pondría en 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}

Actuación:

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

... suspiro, no pienses que es mi culpa ...


[nota histórica sin importancia debido a problemas de moderación]

Con respecto al supuesto duplicado de Flatten, un diccionario de diccionarios (2 niveles de profundidad) de listas en Python :

La solución de esa pregunta se puede implementar en términos de esta haciendo sorted( sum(flatten(...),[]) ). Lo contrario no es posible: si bien es cierto que los valores de flatten(...)se pueden recuperar del supuesto duplicado mapeando un acumulador de orden superior, no se pueden recuperar las claves. (editar: también resulta que la supuesta pregunta del propietario duplicado es completamente diferente, ya que solo trata con diccionarios con una profundidad de 2 niveles, aunque una de las respuestas en esa página ofrece una solución general).

ninjagecko
fuente
2
No estoy seguro de si esto es relevante para la pregunta. Esta solución no aplana un elemento del diccionario de una lista de diccionarios, es decir, {'a': [{'aa': 1}, {'ab': 2}]}. La función flattenDict se puede modificar fácilmente para acomodar este caso.
Stewbaca
56

O si ya está usando pandas, puede hacerlo json_normalize()así:

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

Salida:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
MYGz
fuente
55
o simplemente pasar el alegación de SEP :)
Blue Moon
3
Es una pena que no maneje listas :)
Roelant
32

Si está utilizando, pandashay una función oculta en pandas.io.json._normalize1 llamada nested_to_recordque hace esto exactamente.

from pandas.io.json._normalize import nested_to_record    

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

1 En versiones de pandas 0.24.xy uso anterior pandas.io.json.normalize(sin el _)

Aaron N. Brock
fuente
2
Lo que funcionó para mí fue from pandas.io.json._normalize import nested_to_record. Observe el guión bajo ( _) antes normalize.
Eyal Levin el
3
@EyalLevin ¡Buena captura! Esto cambió en 0.25.x, he actualizado la respuesta. :)
Aaron N. Brock el
29

Aquí hay una especie de implementación "funcional", de "línea única". Es recursivo y se basa en una expresión condicional y una comprensión dictada.

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 }

Prueba:

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}
dividir entre cero
fuente
1
Esto no funciona para los diccionarios generales, específicamente, con las teclas de tupla, por ejemplo, sustituye ('hgf',2)la segunda clave en tus lanzamientos de pruebaTypeError
alancalvitti
1
@alancalvitti Esto supone que es una cadena, o algo más que admite el +operador. Para cualquier otra cosa, deberá adaptarse prefix + separator + ka la llamada de función adecuada para componer los objetos.
dividebyzero
1
Otro tema relevante para las claves de tupla. He publicado por separado cómo generalizar según su método. Sin embargo, no puede manejar correctamente el ejemplo de ninjageko:{'a_b':{'c':1}, 'a':{'b_c':2}}
alancalvitti
3
Me estaba preocupando, al no ver respuestas utilizando la recursividad. ¿Qué le pasa a nuestra juventud en estos días?
Jakov
1
no hace nada si un dict tiene una lista anidada de dictados, como esta:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M
13

Código:

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,''))

Resultados:

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

Estoy usando python3.2, actualización para su versión de python.

Pavan Yalamanchili
fuente
Probablemente desee especificar el valor predeterminado de lkey=''en la definición de su función en lugar de cuando llame a la función. Ver otras respuestas al respecto.
Acumenus
7

¿Qué tal una solución funcional y eficiente 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(), 
        {}
    ))

Esto es aún más eficaz:

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(), 
        {}
    ))

En uso:

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
fuente
2
¿Qué tal una solución legible y funcional? ;) ¿En qué versión probaste esto? Recibo un "error de sintaxis" al probar esto en Python 3.4.3. Parece que el uso de "** all" no es legítimo.
Ingo Fischer
Trabajo desde Python 3.5. No sabía que no funciona con 3.4. Tienes razón, esto no es muy legible. Actualicé la respuesta. Espero que sea más legible ahora. :)
Rotareti
1
Se agregó la importación de reducción reducida. Todavía encuentro el código difícil de entender y creo que es un buen ejemplo de por qué el propio Guido van Rossum desalentó el uso de lambda, reducir, filtrar y mapear en 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer
Estoy de acuerdo. Python no está realmente diseñado para la programación funcional . Aún así, creo que reducees genial en caso de que necesite reducir los diccionarios. Actualicé la respuesta. Debería verse un poco más pitón ahora.
Rotareti el
7

Esto no está restringido a los diccionarios, sino a todos los tipos de mapeo que implementan .items (). Además es más rápido ya que evita una condición if. Sin embargo, los créditos van a 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
fuente
2
Si dno se trata de un dicttipo de mapeo personalizado que no se implementa items, su función fallará en ese momento. Por lo tanto, no funciona para todos los tipos de mapeo, sino solo para aquellos que lo implementan items().
user6037143
1
@ user6037143 ¿alguna vez has encontrado un tipo de mapeo que no se implemente items? Me daría curiosidad ver uno.
Trey Hunner
2
@ user6037143, no, por definición, no lo ha hecho si los elementos no están implementados, no es un tipo de mapeo.
Davoud Taghawi-Nejad
1
@ DavoudTaghawi-Nejad, ¿podría modificar esto para manejar claves generales, por ejemplo, tuplas que no deben aplanarse internamente?
alancalvitti
6

Mi solución Python 3.3 usando generadores:

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
fuente
¿Se puede extender para manejar cualquier tipo de clave válida que no sea str (incluida la tupla)? En lugar de concatenación de cadenas, únalas en una tupla.
alancalvitti
5

Utilizando la recursividad, manteniéndola simple y legible por humanos:

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

La llamada es simple:

new_dict = flatten_dict(dictionary)

o

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

si queremos cambiar el separador predeterminado

Un pequeño desglose:

Cuando la función se llama por primera vez, se llama solo pasando el dictionaryque queremos aplanar. El accumulatorparámetro está aquí para admitir la recursividad, que veremos más adelante. Entonces, instanciamos accumulatora un diccionario vacío donde colocaremos todos los valores anidados del original dictionary.

if accumulator is None:
    accumulator = {}

A medida que iteramos sobre los valores del diccionario, construimos una clave para cada valor. El parent_keyargumento será Nonepara la primera llamada, mientras que para cada diccionario anidado, contendrá la clave que lo señala, por lo que anteponemos esa clave.

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

En caso de que el valor val kque apunta la clave sea ​​un diccionario, la función se llama a sí misma, pasando el diccionario anidado, el accumulator(que se pasa por referencia, por lo que todos los cambios realizados en él se hacen en la misma instancia) y la clave kpara que podamos puede construir la clave concatenada. Observe la continuedeclaración. Queremos omitir la siguiente línea, fuera del ifbloque, para que el diccionario anidado no termine en la accumulatortecla debajo k.

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

Entonces, ¿qué hacemos en caso de que el valor vno sea un diccionario? Solo ponlo sin cambios dentro del accumulator.

accumulator[k] = v

Una vez que terminamos, simplemente devolvemos el accumulator, dejando dictionaryintacto el argumento original .

NOTA

Esto funcionará solo con diccionarios que tengan cadenas como claves. Funcionará con objetos hashables implementando el __repr__método, pero producirá resultados no deseados.

Jakov
fuente
4

Función simple para aplanar diccionarios anidados. Para Python 3, reemplace .iteritems()con.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

La idea / requisito era: obtener diccionarios planos sin guardar las claves principales.

Ejemplo de uso:

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}

Mantener las claves primarias también es simple.

Ivy Growing
fuente
3

Esto es similar a la respuesta de imran y ralu. No utiliza un generador, sino que emplea la recursión con un cierre:

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
fuente
No estoy seguro si el uso del término " cierre " es correcto aquí, ya que la función _flatten_dictnunca se devuelve, ni se espera que se devuelva alguna vez. Tal vez se pueda referir a ella como una subfunción o una función cerrada .
Acumenus
3

La solución de Davoud es muy buena, pero no da resultados satisfactorios cuando el dictado anidado también contiene listas de dictados, pero su código debe adaptarse para ese caso:

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)
usuario3830731
fuente
Puede almacenar en caché el resultado de type([])para evitar una llamada de función para cada elemento del dict.
bfontaine
2
Por favor, use isinstance(v, list)en su lugar
Druska
2

Las respuestas anteriores funcionan realmente bien. Solo pensé que agregaría la función unflatten que escribí:

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

Nota: Esto no tiene en cuenta '_' que ya está presente en las teclas, al igual que las contrapartidas aplanados.

tarequeh
fuente
2

Aquí hay un algoritmo para un reemplazo elegante en el lugar. Probado con Python 2.7 y Python 3.5. Usando el carácter de punto como separador.

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

Ejemplo:

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

Salida:

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

Publiqué este código aquí junto con la unflatten_jsonfunción de coincidencia .

Alexander Ryzhov
fuente
2

Si desea aplanar el diccionario anidado y desea una lista de claves única, esta es la solución:

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
fuente
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
fuente
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
fuente
esto funciona con listas dentro de nuestro dict anidado, pero no tiene una opción de separador personalizado
Nikhil VJ
2

Estaba pensando en una subclase de UserDict para aplanar las teclas automáticamente.

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)

‌ Las ventajas es que las claves se pueden agregar sobre la marcha, o utilizando la instancia estándar dict, sin sorpresa:

>>> 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
fuente
1
Asignar a fd ['persona'] pero mantener su valor existente es bastante sorprendente. Así no es como funcionan los dictados regulares.
tbm
1

Usando generadores:

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
fuente
2
type(i).__name__=='dict'podría ser reemplazado con type(i) is dicto quizás incluso mejor isinstance(d, dict)(o Mapping/ MutableMapping).
Cristian Ciupitu
1

Usando dict.popitem () en una recursión directa similar a una lista anidada:

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
fuente
1

No es exactamente lo que pidió el OP, pero mucha gente viene aquí buscando formas de aplanar datos JSON anidados en el mundo real que pueden tener objetos y matrices json de valor clave anidado y objetos json dentro de las matrices, etc. JSON no incluye tuplas, por lo que no tenemos que preocuparnos por ellas.

Encontré una implementación del comentario de inclusión de lista de @roneo a la respuesta publicada por @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)

Pruébalo:

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}

Y eso hace el trabajo que necesito hacer: arrojo cualquier json complicado a esto y lo aplana para mí.

Todos los créditos a https://github.com/ScriptSmith .

Nikhil VJ
fuente
1

¡De hecho, escribí un paquete llamado cherrypicker recientemente para tratar este tipo de cosas exactamente ya que tenía que hacerlo con tanta frecuencia!

Creo que el siguiente código te daría exactamente lo que buscas:

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

Puede instalar el paquete con:

pip install cherrypicker

... y hay más documentos y orientación en https://cherrypicker.readthedocs.io .

Otros métodos pueden ser más rápido, pero la prioridad de este paquete es hacer este tipo de tareas fáciles . Sin embargo, si tiene una gran lista de objetos para aplanar, también puede decirle a CherryPicker que use el procesamiento paralelo para acelerar las cosas.

big-o
fuente
Me gusta el enfoque alternativo.
Gergely M
0

Siempre prefiero acceder a los dictobjetos a través de .items(), por lo que para aplanar dictos utilizo el siguiente generador recursivo flat_items(d). Si desea tener de dictnuevo, simplemente envuélvalo así: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
fuente
0

Variación de este Flatten diccionarios anidados, comprimiendo claves con max_level y reductor personalizado.

  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)
usuario2528473
fuente
0

Si no le importa las funciones recursivas, aquí hay una solución. También me he tomado la libertad de incluir una exclusión parámetro de en caso de que desee mantener uno o más valores.

Código:

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

Uso:

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)

Salida:

{'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
fuente
0

Probé algunas de las soluciones en esta página, aunque no todas, pero las que probé no pudieron manejar la lista anidada de dict.

Considere un dict como este:

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': []
                 }
            ]
        }
    }

Aquí está mi solución improvisada:

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

que produce:

{
  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,
}

Una solución improvisada y no es perfecta.
NOTA:

  • no mantiene dictos vacíos como el address: {}par k / v.

  • no aplanará los dictados en tuplas anidadas, aunque sería fácil agregarlas usando el hecho de que las tuplas de pitón actúan de manera similar a las listas.

Gergely M
fuente
-1

Simplemente use python-benedict, es una subclase dict que ofrece muchas características, incluido un flattenmétodo. Es posible instalarlo usando 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
fuente