Это действительно старый вопрос. Недавно я столкнулся с похожей проблемой.
Это может быть очевидно, но вы имеете дело с деревом, в котором каждый узел имеет произвольное количество потомков. Вы хотите вырезать поддеревья, которые не содержат некоторых элементов в качестве узлов (не листьев). Для этого вы используете специальную 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
s:
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
_dict_key_filter
и какие параметры он принимает? Например, я бы предположил, чтоobj_filter
является вызываемым, но, по-видимому, это последовательность ключей, которая является приемлемой? - person Two-Bit Alchemist   schedule 29.07.2015obj_filter
? На любом уровне? - person Two-Bit Alchemist   schedule 29.07.2015