Поднимите все вхождения типа во вложенном словаре до ключа верхнего уровня

1

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

Пока у меня есть код ниже, который, похоже, работает. В этом примере я ищу все элементы, которые являются целыми числами, и перемещая их на ключ 'numbers'.

Я бы предпочел, чтобы функция lift_numbers_to_top сделала и вернула копию словаря, а не редактировала его на месте, но я не смог разработать отличный способ передать копию и числа назад от рекурсивной функции до если это имеет смысл.

a_dictionary = {
    "one": 1,
    "two": 2,
    "text": "Hello",
    "more_text": "Hi",
    "internal_dictionary": {
        "three": 3,
        "two": 2,
        "even_more_text": "Hey",
        "another_internal_dictionary": {
            "four": 4,
            "five": 5,
            "last_text": "howdy"
        }
    }
}


def extract_integers(dictionary, level_key=None):
    numbers = {}
    for key in dictionary:
        if type(dictionary[key]) == int:
            numbers[level_key + "__" + key if level_key else key] = dictionary[key]
    return numbers


def lift_numbers_to_top(dictionary, level_key=None):
    numbers = {}
    if type(dictionary) == dict:
        numbers = extract_integers(dictionary, level_key)
        for key in numbers:
            keyNumber = key.split('__')[-1]
            del dictionary[keyNumber]
        for key in dictionary:
            numbers = {**numbers, **lift_numbers_to_top(dictionary[key], key)}
    return numbers


a_dictionary['numbers'] = lift_numbers_to_top(a_dictionary)
print(a_dictionary)

Результат:

{
    'text': 'Hello',
    'more_text': 'Hi',
    'internal_dictionary': {
        'even_more_text': 'Hey',
        'another_internal_dictionary': {
            'last_text': 'howdy'
        },
    },
    'numbers': {
        'one': 1,
        'two': 2,
        'internal_dictionary__two': 2,
        'internal_dictionary__three': 3,
        'another_internal_dictionary__four': 4,
        'another_internal_dictionary__five': 5,
    }
}
  • 0
    Пожалуйста, оставьте желаемый результат.
  • 0
    @Sarcoma: это помогает нам определить, понимаем ли мы формулировку проблемы и убедиться, что наше решение соответствует ожиданиям. Пожалуйста, добавьте его в свой пост.
Показать ещё 1 комментарий
Теги:
dictionary

2 ответа

3
Лучший ответ

Используйте функцию сопоставления, чтобы определить, что нужно снять, и пройдите по целевому объекту, где вы перемещаете пары ключ-значение в рекурсивные вызовы. Если эта цель отсутствует, вы знаете, что текущий вызов предназначен для верхнего уровня. Функция соответствия должна возвращать новый ключ для нового словаря.

Чтобы создать новый словарь, просто создайте новый словарь и поместите результаты рекурсии в этот объект.

Я предпочитаю использовать @singledispatch() для обработки разных типов при рекурсии:

from functools import singledispatch

@singledispatch
def lift_values(obj, match, targetname=None, **kwargs):
    """Lift key-value pairs from a nested structure to the top

    For key-value pairs anywhere in the nested structure, if
    match(path, value) returns a value other than 'None', the 
    key-value pair is moved to the top-level dictionary when targetname
    is None, or to a new dictionary stored under targetname is not None,
    using the return value of the match function as the key. path
    is the tuple of all keys and indices leading to the value.

    For example, for an input 

        {'foo': True, 'bar': [{'spam': False, 'ham': 42}]}

    and the match function lambda p, v: p if isinstance(v, bool) else None
    and targetname "flags", this function returns

        {'flags': {('foo',): True, ('bar', 0, 'spam'): False}, 'bar': [{'ham': 42}]}

    """
    # leaf nodes, no match testing needed, no moving of values
    return obj

@lift_values.register(list)
def _handle_list(obj, match, _path=(), **kwargs):
    # list values, no lifting, just passing on the recursive call
    return [lift_values(v, match, _path=_path + (i,), **kwargs)
            for i, v in enumerate(obj)]

@lift_values.register(dict)
def _handle_list(obj, match, targetname=None, _path=(), _target=None):
    result = {}
    if _target is None:
        # this is the top-level object, key-value pairs are lifted to
        # a new dictionary stored at this level:
        if targetname is not None:
            _target = result[targetname] = {}
        else:
            # no target name? Lift key-value pairs into the top-level
            # object rather than a separate sub-object.
            _target = result

    for key, value in obj.items():
        new_path = _path + (key,)
        new_key = match(new_path, value)
        if new_key is not None:
            _target[new_key] = value
        else:
            result[key] = lift_values(
                value, match, _path=new_path, _target=_target)

    return result

Я включил функцию отправки для списков; ваш образец не использует списки, но они распространены в структурах данных JSON, поэтому я ожидаю, что вы, вероятно, захотите его в любом случае.

Функция соответствия должна принимать два аргумента, путь к объекту, в котором была найдена пара ключ-значение, и значение. Он должен вернуть новый ключ для использования или None если не поднять значение.

Для вашего случая функция соответствия:

def lift_integers(path, value):
    if isinstance(value, int):
        return '__'.join(path[-2:])

result = lift_values(a_dictionary, lift_integers, 'numbers')

Демонстрация в вашем примере ввода словаря:

>>> from pprint import pprint
>>> def lift_integers(path, value):
...     if isinstance(value, int):
...         return '__'.join(path[-2:])
...
>>> lift_values(a_dictionary, lift_integers, 'numbers')
{'numbers': {'one': 1, 'two': 2, 'internal_dictionary__three': 3, 'internal_dictionary__two': 2, 'another_internal_dictionary__four': 4, 'another_internal_dictionary__five': 5}, 'text': 'Hello', 'more_text': 'Hi', 'internal_dictionary': {'even_more_text': 'Hey', 'another_internal_dictionary': {'last_text': 'howdy'}}}
>>> pprint(_)
{'internal_dictionary': {'another_internal_dictionary': {'last_text': 'howdy'},
                         'even_more_text': 'Hey'},
 'more_text': 'Hi',
 'numbers': {'another_internal_dictionary__five': 5,
             'another_internal_dictionary__four': 4,
             'internal_dictionary__three': 3,
             'internal_dictionary__two': 2,
             'one': 1,
             'two': 2},
 'text': 'Hello'}

Лично я бы использовал полный путь как ключ в поднятом словаре, чтобы избежать конфликтов имен; либо путем присоединения полного path к новому строковому ключу с каким-то уникальным разделителем, либо просто путем создания path каталога самого нового ключа:

>>> lift_values(a_dictionary, lambda p, v: p if isinstance(v, int) else None, 'numbers')
{'numbers': {('one',): 1, ('two',): 2, ('internal_dictionary', 'three'): 3, ('internal_dictionary', 'two'): 2, ('internal_dictionary', 'another_internal_dictionary', 'four'): 4, ('internal_dictionary', 'another_internal_dictionary', 'five'): 5}, 'text': 'Hello', 'more_text': 'Hi', 'internal_dictionary': {'even_more_text': 'Hey', 'another_internal_dictionary': {'last_text': 'howdy'}}}
>>> pprint(_)
{'internal_dictionary': {'another_internal_dictionary': {'last_text': 'howdy'},
                         'even_more_text': 'Hey'},
 'more_text': 'Hi',
 'numbers': {('internal_dictionary', 'another_internal_dictionary', 'five'): 5,
             ('internal_dictionary', 'another_internal_dictionary', 'four'): 4,
             ('internal_dictionary', 'three'): 3,
             ('internal_dictionary', 'two'): 2,
             ('one',): 1,
             ('two',): 2},
 'text': 'Hello'}
  • 0
    Спасибо за ответ, @singledispatch - это круто, я работаю с тем, что вы мне предоставили, чтобы увидеть, как я могу получить работу. Я добавил желаемые результаты, которые немного отличаются от ваших результатов.
  • 0
    @ Саркома: я обновил свой ответ, чтобы приспособиться к цели. singledispatch здесь не требуется строго, но немного упрощает реализацию, так как вам не нужно делать никаких проверок типов в реализации.
Показать ещё 6 комментариев
0

Вы можете использовать прогулку через dict рекурсивно и всплывать все элементы со значениями как int чтобы создать новый dict

>>> def extract(d):
...     new_d = {}
...     for k,v in d.items():
...         if type(v) == int:
...             new_d[k] = d[k]
...         elif type(v) == dict:
...             for k2,v2 in extract(v).items():
...                 new_d[k2 if '__' in k2 else k+'__'+k2] = v2
...     return new_d
... 
>>> a_dictionary['numbers'] = extract(a_dictionary)
>>> pprint(a_dictionary)
{'internal_dictionary': {'another_internal_dictionary': {'last_text': 'howdy'},
                         'even_more_text': 'Hey'},
 'more_text': 'Hi',
 'numbers': {'another_internal_dictionary__five': 5,
             'another_internal_dictionary__four': 4,
             'internal_dictionary__three': 3,
             'internal_dictionary__two': 2,
             'one': 1,
             'two': 2},
 'text': 'Hello'}
  • 0
    Спасибо, но я не хочу дополнительного вложения. Это для API, чтобы собрать некоторые полезные данные в хороший список.
  • 0
    @Sarcoma. Сожалею. Неправильно прочитанный вопрос. Обновили ответ
Показать ещё 6 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню