Как фильтровать по ключам через вложенный словарь питоническим способом

Попробуйте отфильтровать вложенный словарь. Мое решение неуклюже, я надеялся увидеть, есть ли лучший метод, использующий понимания. Интересует только словарь и списки для этого примера.

_dict_key_filter() будет фильтровать ключи вложенного словаря или списка вложенных словарей. Все, что не входит в obj_filter, будет игнорироваться на всех вложенных уровнях.

obj : может быть словарем или списком словарей.

obj_filter: должен быть списком значений фильтра

def _dict_key_filter(self, obj, obj_filter):
    if isinstance(obj, dict):
        retdict = {}
        for key, value in obj.iteritems():
            if key in obj_filter:
                retdict[key] = copy.deepcopy(value)
            elif isinstance(value, (dict, list)):
                child = self._dict_key_filter(value, obj_filter)
                if child:
                    retdict[key] = child
        return retdict if retdict else None
    elif isinstance(obj, list):
        retlist = []
        for value in list:
            child = self._dict_key_filter(value, obj_filter)
            if child:
                retlist.append(child)
        return retlist if retlist else None
    else:
        return None

Example#
dict1 = {'test1': {'test2':[1,2]}, 'test3': [{'test6': 2}, 
         {'test8': {'test9': 23}}], 'test4':{'test5': 5}}

filter = ['test5' , 'test9']

return = _dict_key_filter(dict1, filter)

return value would be {'test3': [{'test8': {'test9': 23}}], 'test4': {'test5': 5}}

person user1539348    schedule 29.07.2015    source источник
comment
Можете ли вы изменить свой вопрос, указав, что должен делать _dict_key_filter и какие параметры он принимает? Например, я бы предположил, что obj_filter является вызываемым, но, по-видимому, это последовательность ключей, которая является приемлемой?   -  person Two-Bit Alchemist    schedule 29.07.2015
comment
Готово, надеюсь, я разъяснил, если нет, дайте мне знать.   -  person user1539348    schedule 29.07.2015
comment
Это не совсем понятно, потому что вы все еще используете только фильтр слов, не определяя его. По какому механизму должен работать фильтр? Отфильтровать все, что не отображается в obj_filter? На любом уровне?   -  person Two-Bit Alchemist    schedule 29.07.2015
comment
Я обновил пост. obj_filter используется для сравнения с вложенным словарем, любой ключ из узла самого нижнего уровня, которого нет в obj_filter, будет удален. Пожалуйста, смотрите пример внизу.   -  person user1539348    schedule 30.07.2015


Ответы (1)


Это действительно старый вопрос. Недавно я столкнулся с похожей проблемой.

Это может быть очевидно, но вы имеете дело с деревом, в котором каждый узел имеет произвольное количество потомков. Вы хотите вырезать поддеревья, которые не содержат некоторых элементов в качестве узлов (не листьев). Для этого вы используете специальную DFS: основная функция возвращает либо поддерево, либо None. Если значение равно None, то вы "обрезаете" ветвь.

Прежде всего, функция dict_key_filter возвращает (непустое) dict, (непустое) list или None, если в ветке не найден ключ фильтра. Чтобы уменьшить сложность, вы можете вернуть последовательность в каждом случае: пустая последовательность, если ключ фильтра не найден, и непустая последовательность, если вы все еще ищете или нашли лист дерева. Ваш код будет выглядеть так:

def dict_key_filter(obj, obj_filter):
    if isinstance(obj, dict):
        retdict = {}
        ...
        return retdict # empty or not
    elif isinstance(obj, list):
        retlist = []
        ...
        return retlist # empty or not
    else:
        return [] # obvioulsy empty

Это была легкая часть. Теперь нам нужно заполнить точки.

Дело list

Начнем с случая list, так как его проще рефакторить:

retlist = []
for value in obj:
    child = dict_key_filter0(value, obj_filter)
    if child:
        retlist.append(child)

Мы можем перевести это в простое понимание списка:

retlist = [dict_key_filter(value, obj_filter) for value in obj if dict_key_filter(value, obj_filter)]

Недостатком является то, что dict_key_filter оценивается дважды. Мы можем избежать этого с помощью небольшого трюка (см. https://stackoverflow.com/a/15812866):

retlist = [subtree for subtree in (dict_key_filter(value, obj_filter) for value in obj) if subtree]

Внутреннее выражение (dict_key_filter(value, obj_filter) for value in obj) — это генератор, который вызывает dict_key_filter один раз для каждого значения. Но мы можем добиться большего, если построим замыкание dict_key_filter:

def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)

    ...

    retlist = list(filter(len, map(inner_dict_key_filter, obj)))

Теперь мы находимся в функциональном мире: map применяет inner_dict_key_filter к каждому элементу списка, а затем поддеревья фильтруются, чтобы исключить пустые поддеревья (len(subtree) истинно, если и только если subtree не пусто). Теперь код выглядит так:

def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)

    if isinstance(obj, dict):
        retdict = {}
        ...
        return retdict
    elif isinstance(obj, list):
        return list(filter(len, map(inner_dict_key_filter, obj)))
    else:
        return []

Если вы знакомы с функциональным программированием, регистр list удобочитаем (не так, как в Haskell, но все же удобочитаем).

Дело dict

Я не забыл тег dictionary-comprehension в вашем вопросе. Первая идея состоит в том, чтобы создать функцию, возвращающую либо полную копию ветки, либо результат остальной части DFS.

def build_subtree(key, value):
    if key in obj_filter:
        return copy.deepcopy(value) # keep the branch
    elif isinstance(value, (dict, list)):
        return inner_dict_key_filter(value) # continue to search
    return [] # just an orphan value here

Как и в случае с list, пока не отказываемся от пустых subtrees:

retdict = {}
for key, value in obj.items():
    retdict[key] = build_subtree(key, value)

Теперь у нас есть идеальный случай для понимания dict:

retdict = {key: build_subtree(key, value) for key, value in obj.items() if build_subtree(key, value)}

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

retdict = {key:subtree for key, subtree in ((key, build_subtree(key, value)) for key, value in obj.items()) if subtree}

Но у нас есть небольшая проблема: приведенный выше код не совсем эквивалентен исходному коду. Что делать, если значение равно 0? В исходной версии у нас есть retdict[key] = copy.deepcopy(0), но в новой версии ничего нет. Значение 0 оценивается как ложное и фильтруется. И тогда дикт может стать пустым, и мы неправомерно обрежем ветку. Нам нужен еще один тест, чтобы убедиться, что мы хотим удалить значение: если это пустой список или словарь, удалите его, иначе сохраните:

def to_keep(subtree): return not (isinstance(subtree, (dict, list)) or len(subtree) == 0)

То есть:

 def to_keep(subtree): return not isinstance(subtree, (dict, list)) or subtree

Если вы немного помните логику (https://en.wikipedia.org/wiki/Truth_table#Logical_implication), вы можете интерпретировать это так: если subtree — это словарь или список, то он не должен быть пустым.

Давайте сложим кусочки вместе:

def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)
    def to_keep(subtree): return not isinstance(subtree, (dict, list)) or subtree

    def build_subtree(key, value):
        if key in obj_filter:
            return copy.deepcopy(value) # keep the branch
        elif isinstance(value, (dict, list)):
            return inner_dict_key_filter(value) # continue to search
        return [] # just an orphan value here

    if isinstance(obj, dict):
        key_subtree_pairs = ((key, build_subtree(key, value)) for key, value in obj.items())
        return {key:subtree for key, subtree in key_subtree_pairs if to_keep(subtree)}
    elif isinstance(obj, list):
        return list(filter(to_keep, map(inner_dict_key_filter, obj)))
    return []

Я не знаю, более ли это питонично, но мне это кажется более понятным.

dict1 = {
    'test1': { 'test2':[1,2] }, 
    'test3': [
        {'test6': 2}, 
        {
            'test8': { 'test9': 23 }
        }
    ],
    'test4':{'test5': 0}
}

obj_filter = ['test5' , 'test9']

print (dict_key_filter(dict1, obj_filter))
# {'test3': [{'test8': {'test9': 23}}], 'test4': {'test5': 0}}
person jferard    schedule 09.01.2019