Как да филтрирате по ключове през вложен речник по pythonic начин

Опитайте се да филтрирате вложен речник. Моето решение е тромаво, надявах се да видя дали има по-добър метод, използващ разбирания. Интересуват се само от речника и списъците за този пример.

_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, засега не отказваме празни subtree:

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 се оценява като невярна и се филтрира. И тогава dict може да стане празен и да отрежем клона погрешно. Имаме нужда от друг тест, за да сме сигурни, че искаме да премахнем стойност: ако е празен списък или dict, премахнете го, в противен случай го запазете:

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 е dict или списък, тогава той не трябва да е празен.

Нека съберем парчетата заедно:

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