Предположим, у вас есть словарь вроде:
{'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]}
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
Просто используйте python-benedict
, это - подкласс dict, который предлагает много функций, включенных flatten
метод. Возможно установить его с помощью зернышка: pip install python-benedict
https://github.com/fabiocaccamo/python-benedict#flatten
from benedict import benedict
d = benedict(data)
f = d.flatten(separator='_')
Изменение этого Сглаживает вложенные словари, сжимая ключи с 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)
Использование рекурсии, сохраняя это простым и человекочитаемым:
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(dict=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
аргумент нетронутым.
ПРИМЕЧАНИЕ
Это будет работать только со словарями, которые имеют строки как ключи. Это будет работать с hashable объектами, реализовывая __repr__
метод, но приведет к нежелательным результатам.
Я думал о подклассе 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 instanciation, без удивления:
‌
>>> 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'}