← все задачи

Python · задача 3 из 10

Разложить вложенный словарь в плоский

Средний 15 минут рекурсиягенераторыкраевые случаи

Условие

Напишите функцию, которая превращает вложенный словарь в плоский: ключи склеиваются через точку. {"db": {"host": "localhost", "port": 5432}} превращается в {"db.host": "localhost", "db.port": 5432}.

Что требуется

  • Глубина вложенности произвольная
  • Разделитель задаётся параметром, по умолчанию точка
  • Пустой вложенный словарь не должен потерять ключ целиком — обсудите, что с ним делать

Пример

data = {
    "db": {"host": "localhost", "port": 5432},
    "debug": True,
    "cache": {"redis": {"ttl": 60}},
}

flatten(data)
# {"db.host": "localhost", "db.port": 5432,
#  "debug": True, "cache.redis.ttl": 60}

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Что делать со списками: разворачивать по индексам (items.0.name) или оставлять как значение?
  • Что с пустым вложенным словарём — выбросить ключ или оставить {} как значение?
  • Может ли точка встречаться в самих ключах — тогда результат неоднозначен
Показать решение Скрыть решение

Решение

def flatten(data, separator=".", prefix=""):
    result = {}

    for key, value in data.items():
        full_key = f"{prefix}{separator}{key}" if prefix else str(key)

        if isinstance(value, dict) and value:
            result.update(flatten(value, separator, full_key))
        else:
            # пустой словарь — это тоже значение, иначе ключ пропадёт
            result[full_key] = value

    return result


# Вариант на генераторе: не строит промежуточные словари на каждом уровне
def flatten_items(data, separator=".", prefix=""):
    for key, value in data.items():
        full_key = f"{prefix}{separator}{key}" if prefix else str(key)
        if isinstance(value, dict) and value:
            yield from flatten_items(value, separator, full_key)
        else:
            yield full_key, value

Почему так

Почему рекурсия здесь уместна

  • Структура данных сама рекурсивная: словарь внутри словаря — решение повторяет форму задачи
  • Глубина вложенности конфигов — единицы уровней, до предела рекурсии (1000) не дойдёт
  • Если данные приходят извне и глубина не ограничена, скажите об этом и предложите явный стек — это плюс к ответу

Почему isinstance(value, dict) and value

  • Без проверки на пустоту ключ с {} исчез бы из результата — данные потерялись бы молча
  • Молчаливая потеря данных на интеграции — худший вид бага: заметят через месяц и не там
  • Явное решение по краевому случаю ценнее «правильного» ответа: покажите, что он вообще есть

Зачем вариант с генератором

  • Рекурсивный update копирует накопленное на каждом уровне, генератор отдаёт пары по одной
  • Из генератора легко собрать dict(flatten_items(data)) или сразу писать в файл, не держа всё в памяти
  • yield from — короткая форма проброса, и вопрос «чем отличается от yield в цикле» звучит следом очень часто

Что спросят дальше

  • Обратная задача: unflatten — соберите словарь назад, это частое продолжение
  • Спросят про списки: items.0.name — покажите ветку с enumerate, но сначала уточните, нужна ли она
  • Коллизия ключей: {"a.b": 1, "a": {"b": 2}} даст один ключ — назовите это ограничением решения

Следующая задача

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