Элегантный способ удаления полей из вложенных словарей

Мне пришлось удалить некоторые поля из словаря, ключи для этих полей находятся в списке. Итак, я написал эту функцию:

def delete_keys_from_dict(dict_del, lst_keys):
    """
    Delete the keys present in lst_keys from the dictionary.
    Loops recursively over nested dictionaries.
    """
    dict_foo = dict_del.copy()  #Used as iterator to avoid the 'DictionaryHasChanged' error
    for field in dict_foo.keys():
        if field in lst_keys:
            del dict_del[field]
        if type(dict_foo[field]) == dict:
            delete_keys_from_dict(dict_del[field], lst_keys)
    return dict_del

Этот код работает, но он не очень элегантный, и я уверен, что есть лучшее решение.

Ответ 1

Во-первых, я думаю, что ваш код работает, а не не элегантен. Там нет непосредственной причины не использовать код, который вы представили.

Есть несколько вещей, которые могут быть лучше, хотя:

Сравнивая тип

Ваш код содержит строку:

if type(dict_foo[field]) == dict:

Это может быть определенно улучшено. Обычно (см. Также PEP8) вы должны использовать isinstance вместо сравнения типов:

if isinstance(dict_foo[field], dict)

Однако это также вернет True если dict_foo[field] является подклассом dict. Если вы не хотите этого, вы также можете использовать is вместо ==. Это будет незначительно (и, вероятно, незаметно) быстрее.

Если вы также хотите разрешить произвольные объекты типа dict, вы можете пойти еще дальше и проверить, является ли он collections.abc.MutableMapping. Это будет True для подклассов dict и dict и для всех изменяемых отображений, которые явно реализуют этот интерфейс без подкласса dict, например UserDict:

>>> from collections import MutableMapping
>>> # from UserDict import UserDict # Python 2.x
>>> from collections import UserDict  # Python 3.x - 3.6
>>> # from collections.abc import MutableMapping # Python 3.7+
>>> isinstance(UserDict(), MutableMapping)
True
>>> isinstance(UserDict(), dict)
False

Изменение на месте и возвращаемое значение

Обычно функции либо изменяют структуру данных на месте, либо возвращают новую (измененную) структуру данных. Просто упомянуть несколько примеров: list.append, dict.clear, dict.update все изменяют структуру данных на месте и return None. Это облегчает отслеживание того, что делает функция. Однако это не жесткое правило, и всегда есть действительные исключения из этого правила. Однако лично я думаю, что подобная функция не должна быть исключением, и я просто удалил бы return dict_del строку return dict_del и позволил бы ей неявно возвращать None, но YMMV.

Удаление ключей из словаря

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

for key in keys_to_remove:
    try:
        del dict[key]
    except KeyError:
        pass

Это дает дополнительное преимущество, заключающееся в том, что вам не нужно вкладывать два цикла (что может быть медленнее, особенно если количество ключей, которые необходимо удалить, очень велико).

Если вам не нравится пустое, except предложений, вы также можете использовать: contextlib.suppress (требуется Python 3. 4+):

from contextlib import suppress

for key in keys_to_remove:
    with suppress(KeyError):
        del dict[key] 

Имена переменных

Есть несколько переменных, которые я бы переименовал, потому что они не описательны и даже не вводят в заблуждение:

  • delete_keys_from_dict вероятно, должен упомянуть обработку delete_keys_from_dict_recursive, возможно, delete_keys_from_dict_recursive.

  • dict_del звучит как удаленный dict. Я предпочитаю использовать такие имена, как dictionary или dct потому что имя функции уже описывает, что делается со словарем.

  • lst_keys, там же. Я бы, наверное, использовал только keys там. Если вы хотите быть более конкретным, что-то вроде keys_sequence будет иметь больше смысла, потому что оно принимает любую sequence (вам просто нужно иметь возможность повторять ее несколько раз), а не только списки.

  • dict_foo, просто нет...

  • field не совсем подходит, это ключ.

Собираем все вместе:

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

Версия, которая модифицирует на месте (очень похоже на решение Неда Батчелдерса):

from collections import MutableMapping
from contextlib import suppress

def delete_keys_from_dict(dictionary, keys):
    for key in keys:
        with suppress(KeyError):
            del dictionary[key]
    for value in dictionary.values():
        if isinstance(value, MutableMapping):
            delete_keys_from_dict(value, keys)

И решение, которое возвращает новый объект:

from collections import MutableMapping

def delete_keys_from_dict(dictionary, keys):
    keys_set = set(keys)  # Just an optimization for the "if key in keys" lookup.

    modified_dict = {}
    for key, value in dictionary.items():
        if key not in keys_set:
            if isinstance(value, MutableMapping):
                modified_dict[key] = delete_keys_from_dict(value, keys_set)
            else:
                modified_dict[key] = value  # or copy.deepcopy(value) if a copy is desired for non-dicts.
    return modified_dict

Однако он создает только копии словарей, остальные значения не возвращаются как копии, вы можете легко обернуть их в copy.deepcopy (я поместил комментарий в соответствующее место кода), если вы этого хотите.

Ответ 2

def delete_keys_from_dict(dict_del, lst_keys):
    for k in lst_keys:
        try:
            del dict_del[k]
        except KeyError:
            pass
    for v in dict_del.values():
        if isinstance(v, dict):
            delete_keys_from_dict(v, lst_keys)

    return dict_del

Ответ 3

Поскольку вопрос задан изящным способом, я представлю свое универсальное решение для борьбы с вложенными структурами. Сначала установите пакет утилиты boltons с pip install boltons, а затем:

from boltons.iterutils import remap

data = {'one': 'remains', 'this': 'goes', 'of': 'course'}
bad_keys = set(['this', 'is', 'a', 'list', 'of', 'keys'])

drop_keys = lambda path, key, value: key not in bad_keys
clean = remap(data, visit=drop_keys)
print(clean)

# Output:
{'one': 'remains'}

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

На этой странице есть еще много примеров, в том числе те, которые работают с гораздо большими объектами из API Github.

Это чистый-Python, поэтому он работает повсюду и полностью протестирован в Python 2.7 и 3.3+. Лучше всего, я написал это для подобных случаев, так что, если вы найдете случай, который он не обрабатывает, вы можете исправить ошибку, /a > .

Ответ 4

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

def delete_keys_from_dict(dict_del, the_keys):
    """
    Delete the keys present in the lst_keys from the dictionary.
    Loops recursively over nested dictionaries.
    """
    # make sure the_keys is a set to get O(1) lookups
    if type(the_keys) is not set:
        the_keys = set(the_keys)
    for k,v in dict_del.items():
        if k in the_keys:
            del dict_del[k]
        if isinstance(v, dict):
            delete_keys_from_dict(v, the_keys)
    return dict_del

Ответ 5

def delete_keys_from_dict(d, to_delete):
    if isinstance(to_delete, str):
        to_delete = [to_delete]
    if isinstance(d, dict):
        for single_to_delete in set(to_delete):
            if single_to_delete in d:
                del d[single_to_delete]
        for k, v in d.items():
            delete_keys_from_dict(v, to_delete)
    elif isinstance(d, list):
        for i in d:
            delete_keys_from_dict(i, to_delete)
    return d

d = {'a': 10, 'b': [{'c': 10, 'd': 10, 'a': 10}, {'a': 10}], 'c': 1 }
delete_keys_from_dict(d, ['a', 'c']) 

>>> {'b': [{'d': 10}, {}]}

Это решение работает для dict и list в данном вложенном dict. Входной to_delete может быть list из str быть удален или один str.

Обратите внимание, что если вы удалите единственный ключ в dict, вы получите пустой dict.

Ответ 6

Я думаю, что следующее более элегантно:

def delete_keys_from_dict(dict_del, lst_keys):
    if not isinstance(dict_del, dict):
        return dict_del
    return {key:value for key,value in ((key, delete_keys_from_dict(value)) for key,value in dict_del.items()) if key not in lst_keys}

Ответ 7

Используя замечательный код из this и добавьте небольшое выражение:

    def remove_fields(self, d, list_of_keys_to_remove):
        if not isinstance(d, (dict, list)):
            return d
        if isinstance(d, list):
            return [v for v in (self.remove_fields(v, list_of_keys_to_remove) for v in d) if v]
        return {k: v for k, v in ((k, self.remove_fields(v, list_of_keys_to_remove)) for k, v in d.items()) if k not in list_of_keys_to_remove}

Ответ 8

это работает с dict содержащим Iterable (list ,...), который может содержать dict. Python 3. Для Python 2 unicode также должен быть исключен из итерации. Также могут быть некоторые итерации, которые не работают, о которых я не знаю. (т.е. приведет к бесконечной рекурсии)

from collections.abc import Iterable

def deep_omit(d, keys):
    if isinstance(d, dict):
        for k in keys:
            d.pop(k, None)
        for v in d.values():
            deep_omit(v, keys)
    elif isinstance(d, Iterable) and not isinstance(d, str):
        for e in d:
            deep_omit(e, keys)

    return d