Сгладить вложенные словари, сжав ключи - PullRequest
125 голосов
/ 17 мая 2011

Предположим, у вас есть словарь вроде:

{'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': [1, 2, 3]}

Ответы [ 19 ]

177 голосов
/ 17 мая 2011

Практически так же, как вы бы сгладили вложенный список, вам просто нужно проделать дополнительную работу для итерации dict по ключу / значению, создания новых ключей для вашего нового словаря и создания словаря на последнем этапе.

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}
57 голосов
/ 18 мая 2011

Есть два больших соображения, которые должен учитывать оригинальный плакат:

  1. Существуют ли проблемы с блокировкой пространства клавиш? Например, {'a_b':{'c':1}, 'a':{'b_c':2}} приведет к {'a_b_c':???}. Приведенное ниже решение уклоняется от проблемы, возвращая повторяющиеся пары.
  2. Если производительность является проблемой, требуется ли функции ключа-редуктора (которую я здесь называю «объединением») доступ ко всему пути ключа, или она может просто выполнить O (1) на каждом узле в дерево? Если вы хотите сказать «1008», это будет стоить вам O (N ^ 2) времени работы. Однако, если вы хотите сказать nextKey = previousKey+'_'+thisKey, это даст вам O (N) время. Приведенное ниже решение позволяет вам выполнять оба действия (поскольку вы можете просто объединить все ключи, а затем обработать их).

(Производительность вряд ли является проблемой, но я подробно остановлюсь на втором моменте на случай, если кто-то еще захочет: при реализации этого существует множество опасных вариантов. Если вы делаете это рекурсивно и получаете и повторно приносите, или что-нибудь эквивалентное, которое затрагивает узлы более одного раза (что довольно легко случайно сделать), вы выполняете работу O (N ^ 2), а не O (N). Это потому, что, возможно, вы вычисляете ключ a, затем a_1, затем a_1_i ..., а затем вычисление a, затем a_1, затем a_1_ii ..., но на самом деле вам не нужно снова вычислять a_1. Даже если вы не пересчитывает его, повторное получение его (подход «уровень за уровнем») так же плохо. Хороший пример - подумать о производительности на {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Ниже приведена функция, которую я написал flattenDict(d, join=..., lift=...), которая может быть адаптирована для многих целей и может делать то, что вы хотите. К сожалению, довольно трудно сделать ленивую версию этой функции, не неся вышеупомянутых потерь производительности (многие встроенные функции Python, такие как chain.from_iterable, на самом деле не эффективны, что я понял только после всестороннего тестирования трех различных версий этого кода, прежде чем остановиться на этот).

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

Чтобы лучше понять, что происходит, ниже приведена схема для тех, кто не знаком с reduce (слева), также известным как «сложить влево». Иногда он рисуется с начальным значением вместо k0 (не является частью списка, переданного в функцию). Здесь J является нашей join функцией. Мы предварительно обрабатываем каждый k n с 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

На самом деле это то же самое, что и functools.reduce, но когда наша функция делает это со всеми путями дерева.

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

Демонстрация (которую я бы положил в строку документации):

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

Производительность:

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

... вздох, не думай, что это моя вина ...


[неважная историческая справка из-за проблем с модерацией]

Относительно предполагаемого дубликата Свести словарь словарей (глубиной 2 уровня) списков в Python :

Решение этого вопроса можно реализовать с помощью этого sorted( sum(flatten(...),[]) ). Обратное невозможно: хотя верно то, что значения из flatten(...) могут быть восстановлены из предполагаемого дубликата путем сопоставления аккумулятора более высокого порядка, ключи не могут быть восстановлены. (редактировать: также выясняется, что вопрос о предполагаемом дубликате владельца совершенно другой, поскольку он касается только словарей ровно 2-уровневого уровня, хотя один из ответов на этой странице дает общее решение.)

32 голосов
/ 23 января 2017

Или, если вы уже используете панд, вы можете сделать это с помощью json_normalize(), например, так:

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

Вывод:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
22 голосов
/ 29 октября 2013

Это своего рода «функциональная», «однострочная» реализация. Он рекурсивен и основан на условном выражении и понимании слова.

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 }

Тест:

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}
12 голосов
/ 17 мая 2011

Код:

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

Результаты:

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

Я использую python3.2, обновление для вашей версии python.

11 голосов
/ 26 апреля 2018

Если вы используете pandas, в pandas.io.json.normalize скрыта функция, называемая nested_to_record, которая делает это точно.

from pandas.io.json.normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')
6 голосов
/ 26 июля 2012

Это не только словари, но и каждый тип отображения, который реализует .items ().Далее это быстрее, так как избегает условия if.Тем не менее, кредиты идут Имрану:

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)
4 голосов
/ 17 января 2017

Как насчет функционального и производительного решения в 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(), 
        {}
    ))

Это еще более производительно:

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

Используется:

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 голосов
/ 27 мая 2017

Простая функция для выравнивания вложенных словарей.Для Python 3 замените .iteritems() на .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

Идея / требование было: Получить плоские словари без сохранения родительских ключей.

Пример использования:

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}

Хранить родительские ключи также просто.

4 голосов
/ 16 марта 2017

My Python 3.3 Solution с использованием генераторов:

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}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...