Згладьте вкладені словники, стискаючи клавіші


172

Припустимо, у вас є словник типу:

{'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]}

2
також є бібліотека для нього: github.com/ianlini/flatten-dict
Ufos

Відповіді:


220

В основному так само, як ви б вирівняли вкладений список, просто потрібно виконати додаткову роботу для ітерації дікта за ключем / значенням, створення нових ключів для вашого нового словника та створення словника на останньому кроці.

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}

7
Якщо замінити isinstanceз try..exceptблоком, це буде працювати для будь-якого відображення, навіть якщо він не є похідним від dict.
Бьорн Поллекс

1
Змінили його для тестування, collections.MutableMappingщоб зробити його більш загальним. Але для Python <2.6, try..exceptмабуть, найкращий варіант.
Імран

5
Якщо ви хочете, щоб порожні словники збереглися у сплющеній версії, ви можете змінити if isinstance(v, collections.MutableMapping):наif v and isinstance(v, collections.MutableMapping):
tarequeh

3
Зауважте, що new_key = parent_key + sep + k if parent_key else kприпустимо, що ключі завжди є рядками, інакше вони піднімуться TypeError: cannot concatenate 'str' and [other] objects. Однак ви можете це виправити, просто примусивши kдо string ( str(k)) або об'єднавши ключі в кортеж замість рядка (кортежі також можуть бути диктантними ключами).
Скотт Н

1
А функція
надуття

65

Є два важливі міркування, які слід враховувати в оригінальному плакаті:

  1. Чи є проблеми з клобіруванням у просторі клавіш? Наприклад, {'a_b':{'c':1}, 'a':{'b_c':2}}це призведе до {'a_b_c':???}. Наведене нижче рішення ухиляється від проблеми, повертаючи ітерабельний пар.
  2. Якщо продуктивність є проблемою, чи вимагає функція редуктора ключів (яку я називаю як "приєднатись") до доступу до всього шляху ключа, чи це може просто виконати роботу O (1) на кожному вузлі дерева? Якщо ви хочете сказати joinedKey = '_'.join(*keys), це обійдеться вам за час роботи (O ^ N). Однак якщо ви готові сказати 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=...)яка може бути адаптована до багатьох цілей і може робити все, що завгодно. На жаль, досить важко зробити ледачу версію цієї функції, не несучи вищезазначених штрафних санкцій (багато вбудованих пітонів, таких як ланцюг.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)

Демонстрація (яку я б інакше вклав у 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}

Продуктивність:

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

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


[неважлива історична примітка через проблеми з модерацією]

Щодо передбачуваного дубліката Flatten - це словник словників (глибиною 2 рівня) списків на Python :

Вирішення цього питання може бути реалізовано з точки зору цього, виконуючи це sorted( sum(flatten(...),[]) ). Зворотне неможливо: в той час як це вірно , що значення з flatten(...)можуть бути вилучені з передбачуваного дубліката шляхом зіставлення акумулятора вищого порядку, не може відновити ключі. (ред .: Також виявляється, що питання передбачуваного дубліката власника зовсім інше, оскільки воно стосується лише словників точно на 2 рівні, хоча один із відповідей на цій сторінці дає загальне рішення.)


2
Я не впевнений, чи це стосується питання. Це рішення не згладжує елемент словника зі списку словників, тобто {'a': [{'aa': 1}, {'ab': 2}]}. Функцію flattenDict можна легко змінити, щоб відповідати цьому випадку.
Стюбака

55

Або якщо ви вже використовуєте панди, ви можете це зробити 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]}

4
або просто передайте аргумент sep :)
Blue Moon

2
Трохи сорому, що це не обробляє списки :)
Roelant

31

Якщо ви використовуєте pandas, є функція, прихована в pandas.io.json._normalize1, що називається, nested_to_recordщо робить саме це.

from pandas.io.json._normalize import nested_to_record    

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

1 У версіях панд 0.24.xта старіших версіях pandas.io.json.normalize(без _)


1
Що для мене працювало from pandas.io.json._normalize import nested_to_record. Помітьте підкреслення ( _) раніше normalize.
Еял Левін

2
@EyalLevin Хороший улов! Це змінилося 0.25.x, і я оновив відповідь. :)
Аарон Н. Брок

28

Ось така собі "функціональна", "однолінійна" реалізація. Він є рекурсивним і заснований на умовному вираженні та розумінні дикту.

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}

Це не працює для загальних словників, зокрема, з кортежними ключами, наприклад, заміна ('hgf',2)другого ключа у ваших тестових TypeError
закидах

@alancalvitti Це передбачає, що це рядок або щось інше, що підтримує +оператора. Для всього іншого вам потрібно буде адаптуватися prefix + separator + kдо відповідного виклику функції для складання об'єктів.
ділбізеро

Ще одне питання, що стосується кортежних ключів. Я окремо розміщував, як узагальнити на основі вашого методу. Однак він не може правильно впоратися з прикладом {'a_b':{'c':1}, 'a':{'b_c':2}}
ніндзягеко

2
Я хвилювався, не бачачи відповідей, використовуючи рекурсію. Що не так у нашій молоді сьогодні?
Яків

нічого не робить, якщо дикт вклав список диктів, як це:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Гергелі М

12

Код:

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.


Можливо, ви хочете вказати значення за замовчуванням lkey=''у своєму визначенні функції замість того, щоб викликати функцію. Дивіться інші відповіді з цього приводу.
Acumenus

6

Як щодо функціонального та ефективного рішення в 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}

2
Як щодо читабельного та робочого рішення? ;) На якій версії ви тестували це? Я отримую "помилку синтаксису" при спробі цього виправити в Python 3.4.3. Здається, використання "** всіх" не є законним.
Інго Фішер

Я працюю з Python 3.5. Не знав, що це не працює з 3.4. Ти маєш рацію, це не дуже читабельно. Я оновив відповідь. Сподіваюсь, це зараз читабельніше. :)
Ротарети

1
Додано відсутнє зменшення імпорту. І все-таки знайти код важко для розуміння, і я думаю, що це хороший приклад, чому сам Гвідо ван Россум відмовлявся від використання лямбда, зменшення, фільтрації та карти вже у 2005 році: artima.com/weblogs/viewpost.jsp?thread=98196
Інго Фішер

Я згоден. Python насправді не розроблений для функціонального програмування . Я все-таки думаю, що reduceце чудово, якщо вам потрібно скоротити словники. Я оновив відповідь. Слід виглядати трохи пітонічніше зараз.
Ротарети

6

Це обмежується не словниками, а кожним типом відображення, що реалізує .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)

1
Якщо dце не dictвласний тип відображення, який не реалізується items, то ваша функція буде відмовлятися тут і там. Отже, це працює не для кожного типу картографування, а лише для тих, хто реалізує items().
користувач6037143

@ user6037143 Ви коли-небудь стикалися з типом відображення, який не реалізується items? Мені було б цікаво побачити.
Трей

1
@ user6037143, ні у вас немає за визначенням, якщо елементи не реалізовані, це не тип відображення.
Davoud Taghawi-Nejad

@ DavoudTaghawi-Nejad, чи можете ви модифікувати це для обробки загальних клавіш, наприклад, кортежів, які не повинні бути внутрішньо сплющеними.
alancalvitti

5

My Python 3.3 Рішення з використанням генераторів:

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}

Ви можете розширити обробку будь-якого дійсного типу ключа, крім str (включаючи кортеж)? Замість з'єднання рядків з'єднайте їх у кортежі.
alancalvitti

4

Проста функція для вирівнювання вкладених словників. Для 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

Використовуючи рекурсію, зберігаючи її простою та зрозумілою для людини:

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

Дзвінок простий:

new_dict = flatten_dict(dictionary)

або

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

якщо ми хочемо змінити роздільник за замовчуванням.

Невелика розбивка:

Коли функція вперше викликається, вона викликається лише передачею, яку dictionaryми хочемо вирівняти. accumulatorПараметр тут для підтримки рекурсії, яку ми побачимо пізніше. Отже, ми створюємо accumulatorпорожній словник, куди ми помістимо всі вкладені значення з оригіналу dictionary.

if accumulator is None:
    accumulator = {}

Коли ми повторюємо значення словника, ми будуємо ключ для кожного значення. parent_keyАргумент буде Noneдля першого виклику, в той час як для кожного вкладеного словника, він буде містити ключ , який вказує на нього, так що ми випереджати цей ключ.

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

Якщо значення, vна kяке вказує ключ, є словником, функція викликає себе, передаючи вкладений словник, accumulator(який передається посиланням, тому всі зміни, зроблені до нього, робляться в тому самому екземплярі) і ключ, kщоб ми може побудувати з'єднаний ключ. Помітьте continueзаяву. Ми хочемо пропустити наступний рядок поза ifблоком, щоб вкладений словник не опинився accumulatorпід клавішею under k.

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

Отже, що ми робимо, якщо значення vне є словником? Просто помістіть його незмінним всередині accumulator.

accumulator[k] = v

Як тільки ми закінчимо, ми просто повернемо accumulator, залишивши оригіналdictionary аргумент недоторканим.

ПРИМІТКА

Це буде працювати лише зі словниками, які містять рядки як ключі. Він буде працювати з доступними об'єктами, що реалізують __repr__метод, але дасть небажані результати.


3

Це схоже і на відповідь імрана і на ралу. Він не використовує генератор, а натомість використовує рекурсію із закриттям:

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}

Я не впевнений, чи правильно тут використовується термін " закриття ", оскільки функція _flatten_dictніколи не повертається, і не очікується, що вона коли-небудь повернеться. Можливо, це може називатися замість нього як підфункція або додана функція .
Acumenus

3

Рішення Даву дуже приємне, але не дає задовільних результатів, коли вкладений дикт також містить списки диктів, але його код буде адаптований для цього випадку:

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)

Ви можете кешувати результат, type([])щоб уникнути виклику функції для кожного елемента dict.
bfontaine

2
Будь ласка, використовуйте isinstance(v, list)замість цього
Друська

2

Наведені вище відповіді дуже добре працюють. Просто подумав, що я додам незакріплену функцію, яку я написав:

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

Примітка. Це не враховує "_", яке вже присутнє в ключах, як і зрівняти аналоги.


2

Ось алгоритм елегантної заміни на місці. Тестували з Python 2.7 та Python 3.5. Використання символу крапки як роздільника.

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

Приклад:

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

Вихід:

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

Я опублікував цей код тут разом із unflatten_jsonфункцією відповідності .


2

Якщо ви хочете вирівняти вкладений словник і хочете перелік усіх унікальних ключів, ось ось таке рішення:

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

це працює зі списками всередині нашого вкладеного дикта, але не має спеціальної опції розділення
Nikhil VJ

2

Я думав про підклас UserDict для автоматичного вирівнювання клавіш.

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)

Переваги цього ключа в тому, що клавіші можна додавати під час руху, або використовуючи стандартний екземпляр, без здивування:

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

1
Присвоєння fd ['person'], але збереження його наявного значення є досить дивовижним. Це не так, як працюють регулярні дикти.
tbm

1

Використання генераторів:

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}

2
type(i).__name__=='dict'може бути замінений type(i) is dictабо, можливо, навіть кращим isinstance(d, dict)(або Mapping/ MutableMapping).
Крістіан Цюпіту

1

Використання dict.popitem () для прямолінійної вкладеної списку:

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

Не зовсім те, що запитувала ОП, але багато людей приходять сюди, шукаючи способів згладити в реальному світі вкладені дані JSON, які можуть вкласти об’єкти json з ключовими значеннями та масиви та об’єкти json всередині масивів тощо. JSON не включає кортежі, тому нам не доведеться хвилюватися за них.

Я знайшов реалізацію коментаря щодо включення до списку від @roneo до відповіді, опублікованої @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)

Перевірте:

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}

І він виконує ту роботу, яку мені потрібно зробити: я кидаю на це будь-який складний json, і це мені це розгладжує.

Усі кредити на https://github.com/ScriptSmith .


1

Нещодавно я написав пакунок під назвою cherrypicker, щоб розібратися з такою точною річчю, оскільки мені довелося це робити так часто!

Я думаю, що наступний код дасть вам саме те, що ви шукаєте:

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

Ви можете встановити пакет за допомогою:

pip install cherrypicker

... і на веб-сторінці https://cherrypicker.readthedocs.io є більше документів та рекомендацій .

Інші методи можуть бути швидше, але пріоритет цього пакета , щоб зробити такі завдання легко . Якщо у вас є великий список об'єктів, які потрібно згладити, ви також можете сказати CherryPicker використовувати паралельну обробку для прискорення роботи.


Мені подобається альтернативний підхід.
Гергелі М

0

Я завжди віддаю перевагу dictоб'єктам доступу через .items(), тому для вирівнювання диктів я використовую наступний рекурсивний генератор flat_items(d). Якщо вам подобається dictзнову, просто загорніть так: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

0

Варіант цього словника Flatten вкладений, стискаючи клавіші з max_level та спеціальним редуктором.

  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)

0

Якщо ви не проти рекурсивних функцій, ось рішення. Я також взяв на себе можливість включити виключення у випадку, якщо є одне або більше значень, які ви хочете зберегти.

Код:

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

Використання:

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)

Вихід:

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

0

Я спробував деякі рішення на цій сторінці - хоча і не всі - але ті, з якими я намагався, не впоралися з вкладеним списком дикту.

Розглянемо такий крес:

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

Ось моє імпровізоване рішення:

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

яка виробляє:

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

Самостійне рішення, і це не ідеально.
ПРИМІТКА:

  • він не зберігає порожні дикти, такі як address: {}пара k / v.

  • він не буде вирівнювати дикти в вкладених кортежах - хоча було б легко додати, використовуючи той факт, що кортежі python діють аналогічно спискам.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.