Това е наистина стар въпрос. Наскоро се натъкнах на подобен проблем.
Може би е очевидно, но имате работа с дърво, в което всеки възел има произволен брой деца. Искате да изрежете поддърветата, които не съдържат някои елементи като възли (не листа). За да постигнете това, вие използвате персонализиран 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
_dict_key_filter
и какви параметри приема? Например, бих предположил, чеobj_filter
е извикваем, но очевидно това е поредица от ключове, които са приемливи? - person Two-Bit Alchemist   schedule 29.07.2015obj_filter
? На всяко ниво? - person Two-Bit Alchemist   schedule 29.07.2015