Сгладить вложенные словари Python, сжимая ключи
Предположим, у вас есть словарь, как:
{'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]}
33 ответа
По сути, точно так же, как вы бы выровняли вложенный список, вам просто нужно проделать дополнительную работу для итерации 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}
Или, если вы уже используете панды, вы можете сделать это с json_normalize()
вот так:
import pandas as pd
df = pd.io.json.json_normalize({
'a': 1,
'c': {
'a': 2,
'b': {
'x': 5,
'y' : 10
}
},
'd': [1, 2, 3]
})
print(df.to_dict(orient='records')[0])
Выход:
{'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd': [1, 2, 3]}
Если не хочешь .
разделитель, вы можете взломать его так:
d = df.to_dict(orient='records')[0]
print dict(zip([x.replace('.', '_') for x in d.keys()], d.values()))
Выход:
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
Есть два больших соображения, которые должен учитывать оригинальный постер:
- Существуют ли проблемы со стеснением клавиш? Например,
{'a_b':{'c':1}, 'a':{'b_c':2}}
приведет к{'a_b_c':???}
, Приведенное ниже решение уклоняется от проблемы, возвращая повторяющиеся пары. - Если производительность является проблемой, требует ли функция ключа-редуктора (которую я здесь называю "объединением") доступа ко всему пути ключа или она может просто выполнять O(1) на каждом узле дерева? Если вы хотите быть в состоянии сказать
joinedKey = '_'.join(*keys)
, это будет стоить вам 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
функция. Мы предварительно обрабатываем каждый kn с 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
... вздох, не думай, что это моя вина...
[неважная историческая справка из-за проблем с модерацией]
Относительно предполагаемого дубликата Flatten словарь словарей (2 уровня глубиной) списков в Python:
Решение этого вопроса может быть реализовано в терминах этого, выполнив sorted( sum(flatten(...),[]) )
, Обратное невозможно: хотя верно, что значения flatten(...)
может быть восстановлен из предполагаемого дубликата путем сопоставления аккумулятора более высокого порядка, невозможно восстановить ключи. (edit: Также оказывается, что вопрос о предполагаемом дубликате владельца совершенно другой, в том смысле, что он имеет дело только со словарями ровно 2-уровневого уровня, хотя один из ответов на этой странице дает общее решение.)
Если вы используете pandas
в скрытой функции pandas.io.json.normalize
называется nested_to_record
который делает это точно.
from pandas.io.json.normalize import nested_to_record
flat = nested_to_record(my_dict, sep='_')
Не совсем то, о чем спрашивал OP, но многие люди приходят сюда в поисках способов сгладить реальные вложенные данные JSON, которые могут иметь вложенные объекты json и массивы ключей и значений, а также объекты json внутри массивов и так далее. JSON не включает кортежи, поэтому нам не о чем беспокоиться.
Я нашел реализацию комментария @roneo о включении в список к ответу, опубликованному @Imran:
https://github.com/ScriptSmith/socialreaper/blob/master/socialreaper/tools.py
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.
Это своего рода "функциональная", "однострочная" реализация. Он рекурсивный и основан на условном выражении и понимании слова.
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}
Код:
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.
Это относится не только к словарям, но и к каждому типу отображения. Далее это быстрее, так как избегает условия 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)
Как насчет функционального и производительного решения в 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}
Если вы поклонник pythonic oneliners:
my_dict={'a': 1,'c': {'a': 2,'b': {'x': 5,'y' : 10}},'d': [1, 2, 3]}
list(pd.json_normalize(my_dict).T.to_dict().values())[0]
возвращает:
{'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd': [1, 2, 3]}
Вы можете оставить
[0]
с конца, если у вас есть список словарей, а не только один словарь.
Использование рекурсии, простота и удобочитаемость:
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
под ключ k
.
if isinstance(v, dict):
flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
continue
Итак, что делать, если значение v
это не словарь? Просто положите его без изменений внутрьaccumulator
.
accumulator[k] = v
Как только мы закончим, мы просто вернем accumulator
, оставив оригинал dictionary
аргумент нетронутым.
НОТА
Это будет работать только со словарями, в которых в качестве ключей используются строки. Он будет работать с хешируемыми объектами, реализующими__repr__
метод, но приведет к нежелательным результатам.
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}
Вот решение с использованием стека. Без рекурсии.
def flatten_nested_dict(nested):
stack = list(nested.items())
ans = {}
while stack:
key, val = stack.pop()
if isinstance(val, dict):
for sub_key, sub_val in val.items():
stack.append((f"{key}_{sub_key}", sub_val))
else:
ans[key] = val
return ans
Я думал о подклассе 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)
Преимущества в том, что ключи можно добавлять на лету или с использованием стандартного экземпляра dict, что не вызывает удивления:
>>> 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'}
Простая функция для выравнивания вложенных словарей. Для 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}
Хранить родительские ключи также просто.
Решение Давуда очень приятно, но не дает удовлетворительных результатов, когда вложенный диктат также содержит списки диктов, но его код должен быть адаптирован для этого случая:
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)
Ответы выше работают очень хорошо. Просто подумал, что я добавлю функцию unlatten, которую я написал:
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
Примечание: это не учитывает '_', уже присутствующее в ключах, во многом как сплющенные аналоги.
На самом деле я недавно написал пакет под названием 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 использовать параллельную обработку для ускорения работы.
def flatten(unflattened_dict, seperator='_'):
flattened_dict = {}
for k, v in unflattened_dict.items():
if isinstance(v, dict):
sub_flattened_dict = flatten(v, "_")
for k2, v2 in sub_flattened_dict.items():
flattened_dict[k+seperator+k2] = v2
else:
flattened_dict[k] = v
return flattened_dict
Использование библиотеки flatdict:
dic={'a': 1,
'c': {'a': 2,
'b': {'x': 5,
'y' : 10}},
'd': [1, 2, 3]}
import flatdict
f = flatdict.FlatDict(dic,delimiter='_')
print(f)
#output
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}
Использование генераторов:
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}
Это похоже на ответ Имрана и Ралу. Он не использует генератор, но вместо этого использует рекурсию с замыканием:
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}
Если вы хотите создать вложенный словарь и получить список всех уникальных ключей, то вот решение:
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))
Вот алгоритм для элегантной замены на месте. Протестировано с 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
функция.
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
Я всегда предпочитаю доступ 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: https://stackru.com/questions/6027558/flatten-nested-python-dictionaries-compressing-keys
>>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
>>> flat = dict(flat_items(example, key_separator='_'))
>>> assert flat['c_b_y'] == 10
"""
for k, v in d.items():
if type(v) is dict:
for k1, v1 in flat_items(v, key_separator=key_separator):
yield key_separator.join((k, k1)), v1
else:
yield k, v
Я пробовал некоторые решения на этой странице - хотя и не все - но те, которые я пытался, не смогли обработать вложенный список dict.
Рассмотрим такой диктат:
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: {}
к / х пара.он не будет сглаживать словарный запас во вложенных кортежах - хотя его было бы легко добавить, используя тот факт, что кортежи Py thon действуют аналогично спискам.
Если вы не против рекурсивных функций, вот решение. Я также взял на себя смелость включить параметр исключения на случай, если вы хотите сохранить одно или несколько значений.
Код:
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}}
Использование 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)}
def flatten(dictionary, prefix = '', separator = '_'):
out_dict = {}
if type(dictionary) != dict:
out_dict[prefix] = dictionary
return out_dict
elif dictionary is None:
return None
for k in dictionary.keys():
if prefix:
prefix_n = prefix + f'{separator}{k}'
else:
prefix_n = k
out_dict.update(flatten_new(dictionary[k], prefix_n))
return out_dict
Выход:
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}