Python: эффективная работа с глубокими вложенностями

Раздел: Основы Python -> Структуры данных

Основные подходы к работе с вложенными структурами

Рекурсивный обход вложенных структур

Как выполнить обход всех элементов во вложенном словаре произвольной глубины?

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

def flatten_dict(d, parent_key=''):
    for k, v in d.items():
        new_key = f'{parent_key}.{k}' if parent_key else k
        if isinstance(v, dict):
            yield from flatten_dict(v, new_key)
        else:
            yield new_key, v

data = {'a': 1, 'b': {'c': 2, 'd': {'e': 3}}}
for path, value in flatten_dict(data):
    print(path, '->', value)

значения списка числа python (итерация по значениям списка чисел в python)

a -> 1
b.c -> 2
b.d.e -> 3

словарь set python (словарь и set в python)

Пояснение: функция проверяет, является ли значение словарём. Если да – рекурсивно спускается, накапливая путь. Иначе – возвращает пару (путь, значение). Так можно обработать структуру любой глубины.

Возникающие проблемы и способы их решения:

Рекурсия ограничена глубиной стека Python (обычно около 1000). Для очень глубоких структур (более 500 уровней) используют итеративную реализацию с явным стеком. Пример:

def flatten_iter(d):
    stack = [(d, '')]
    while stack:
        obj, parent = stack.pop()
        for k, v in obj.items():
            new_key = f'{parent}.{k}' if parent else k
            if isinstance(v, dict):
                stack.append((v, new_key))
            else:
                yield new_key, v

Python dict set (словарь и множество в python)

Прямая индексация и защищённое обращение

Как безопасно получить значение по цепочке ключей, не вызвав ошибку KeyError?

Используется метод get для каждого уровня в цикле или цепочка вызовов с проверкой на None.

data = {'a': {'b': {'c': 42}}}
# Небезопасно: data['a']['b']['x'] вызовет KeyError
value = data.get('a', {}).get('b', {}).get('c')
print(value)  # 42
value_missing = data.get('a', {}).get('b', {}).get('x')
print(value_missing)  # None

типы структур python (типы структур данных в python)

42
None

вложенные структуры данных в python (вложенные структуры данных в python)

Такой подход работает для структур с однотипными ключами. Если ключи могут отсутствовать на любом уровне – это простой и читаемый вариант.

Проблемы:

Цепочка get становится громоздкой при большой глубине. Альтернатива – использование try-except:

try:
    value = data['a']['b']['x']
except KeyError:
    value = None

кортеж чисел python (кортеж чисел в python)

Однако это скрывает другие возможные ошибки (например, если data не словарь).

Изменение значения по пути

Как установить новое значение вложенного элемента, зная путь в виде списка ключей?

Для изменения значения на заданном пути удобно использовать цикл с временной переменной, перемещающейся по уровням.

def set_nested(d, path, value):
    current = d
    for key in path[:-1]:
        current = current[key]
    current[path[-1]] = value

data = {'a': {'b': {'c': 1}}}
set_nested(data, ['a', 'b', 'c'], 100)
print(data)  # {'a': {'b': {'c': 100}}}

язык программирования python массивы (массивы (списки) в python)

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

Проблемы:

Если промежуточный ключ отсутствует – возникнет KeyError. Для автоматического создания отсутствующих словарей используют collections.defaultdict или проверку.

def set_nested_safe(d, path, value):
    current = d
    for key in path[:-1]:
        if key not in current:
            current[key] = {}
        current = current[key]
    current[path[-1]] = value

массивы данных python 3 (массивы данных в python)

Глубокое копирование вложенных структур

Как создать независимую копию вложенного объекта, чтобы изменения в копии не затрагивали оригинал?

Обычное присваивание (new_data = data) создаёт только ссылку на тот же объект. Для полного копирования с обходом всех уровней используется copy.deepcopy.

import copy

data = [{'name': 'Alice', 'scores': [1, 2]}, {'name': 'Bob', 'scores': [3]}]
data_copy = copy.deepcopy(data)
data_copy[0]['scores'].append(3)
print(data)        # [{'name': 'Alice', 'scores': [1, 2]}, {'name': 'Bob', 'scores': [3]}]
print(data_copy)   # [{'name': 'Alice', 'scores': [1, 2, 3]}, {'name': 'Bob', 'scores': [3]}]

одномерные массивы на языке программирования python (одномерные массивы в python)

Без deepcopy изменение data_copy[0]['scores'] затронуло бы оригинал, так как вложенные списки являются мутабельными объектами.

Проблемы:

Глубокое копирование может быть ресурсоёмким для больших структур, включая циклические ссылки. deepcopy обрабатывает циклы корректно, но замедляет работу. Для неизменяемых данных можно ограничиться поверхностным копированием.

Использование библиотеки dpath для JSON‑подобных структур

Как получить или изменить вложенные значения с помощью строки пути, как в файловых системах?

Библиотека dpath предоставляет функции dpath.get, dpath.set, dpath.search для работы с глубокими словарями.

import dpath.util

data = {'a': {'b': {'c': 1, 'd': [2, 3]}}}
# Получение значения
value = dpath.util.get(data, 'a/b/c')
print(value)  # 1
# Установка значения
dpath.util.set(data, 'a/b/e', 4)
print(data)
# {'a': {'b': {'c': 1, 'd': [2, 3], 'e': 4}}}

последовательности в python и способы их реализации (последовательности в python и способы их реализации)

1
{'a': {'b': {'c': 1, 'd': [2, 3], 'e': 4}}}

Библиотека поддерживает glob‑шаблоны, позволяя обрабатывать сразу несколько путей.

Проблемы:

dpath не входит в стандартную библиотеку, требуется установка (pip install dpath). Для проектов без внешних зависимостей предпочтительнее написать собственные функции.

- Python разница списков (разница между списками и кортежами в python)
- как сделать массив python (создание массива (списка) в python)
- списки в языке python (списки в python)

Дополнительные расширенные примеры

Обход вложенных списков разных типов

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

Пример
def flatten_mixed(obj, path=''):
    if isinstance(obj, dict):
        for k, v in obj.items():
            new_path = f'{path}.{k}' if path else str(k)
            yield from flatten_mixed(v, new_path)
    elif isinstance(obj, list):
        for idx, item in enumerate(obj):
            new_path = f'{path}[{idx}]'
            yield from flatten_mixed(item, new_path)
    else:
        yield path, obj

data = {
    'users': [
        {'name': 'Alice', 'age': 30},
        {'name': 'Bob', 'age': 25}
    ],
    'config': {'debug': True}
}
for k, v in flatten_mixed(data):
    print(f'{k} = {v}')
users[0].name = Alice
users[0].age = 30
users[1].name = Bob
users[1].age = 25
config.debug = True

Пояснение: для словарей добавляется ключ, для списков – индекс в квадратных скобках. Такой универсальный обход полезен при анализе JSON или YAML.

Фильтрация вложенных данных с сохранением структуры

Требуется удалить все элементы, значение которых равно None, сохранив пустые словари/списки, если они не пусты после удаления.

Пример
def clean_none(obj):
    if isinstance(obj, dict):
        return {k: clean_none(v) for k, v in obj.items() if v is not None or isinstance(v, (dict, list)) and clean_none(v)}
    elif isinstance(obj, list):
        return [clean_none(item) for item in obj if item is not None or isinstance(item, (dict, list)) and clean_none(item)]
    else:
        return obj

data = {'a': None, 'b': {'c': None, 'd': 1}, 'e': [None, {'f': None}]}
cleaned = clean_none(data)
print(cleaned)  # {'b': {'d': 1}, 'e': []}

Пояснение: рекурсия обрабатывает каждый уровень, удаляя ключи или элементы со значением None. Если после очистки вложенной структуры не остаётся элементов, она тоже удаляется.

Итеративный обход с явным стеком (без рекурсии)

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

Пример
def find_strings_iter(data):
    stack = [(data, '')]
    while stack:
        node, path = stack.pop()
        if isinstance(node, dict):
            for k, v in node.items():
                new_path = f'{path}.{k}' if path else k
                stack.append((v, new_path))
        elif isinstance(node, list):
            for idx, item in enumerate(node):
                new_path = f'{path}[{idx}]'
                stack.append((item, new_path))
        else:
            if isinstance(node, str):
                yield path, node

data = {'a': 1, 'b': 'hello', 'c': {'d': 'world', 'e': [1, '!']}}
for path, val in find_strings_iter(data):
    print(path, ':', val)
c.e[1] : !
c.d : world
b : hello

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

Преобразование вложенной структуры в SQL-подобный плоский вид

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

Пример
users = [
    {'id': 1, 'name': 'Alice', 'scores': {'math': 90, 'eng': 85}},
    {'id': 2, 'name': 'Bob', 'scores': {'math': 78, 'eng': 92}},
]
flat_rows = []
for u in users:
    row = {}
    for path, val in flatten_dict(u):
        row[path] = val
    flat_rows.append(row)
import pandas as pd
df = pd.DataFrame(flat_rows)
print(df)
   id   name  scores.math  scores.eng
0   1  Alice           90          85
1   2    Bob           78          92

Пояснение: функция flatten_dict из первого примера превращает каждый вложенный словарь в плоские колонки. Затем DataFrame даёт удобное табличное представление.

Слияние двух вложенных словарей

Необходимо объединить два глубоких словаря, при этом значения из второго перезаписывают значения первого, а новые ключи добавляются.

Пример
def merge_deep(d1, d2):
    result = d1.copy()
    for key, val in d2.items():
        if key in result and isinstance(result[key], dict) and isinstance(val, dict):
            result[key] = merge_deep(result[key], val)
        else:
            result[key] = val
    return result

d1 = {'a': {'b': 1, 'c': 2}, 'd': 3}
d2 = {'a': {'b': 10, 'e': 4}, 'f': 5}
merged = merge_deep(d1, d2)
print(merged)  # {'a': {'b': 10, 'c': 2, 'e': 4}, 'd': 3, 'f': 5}
{'a': {'b': 10, 'c': 2, 'e': 4}, 'd': 3, 'f': 5}

Пояснение: рекурсивно обходится второй словарь. Если ключ есть в обоих и оба значения – словари, происходит глубокое слияние, иначе значение из второго заменяет первое.

Поиск всех ключей с заданным значением

Задача: найти все пути, где листовое значение равно заданному. Используем рекурсию с накоплением пути.

Пример
def find_value(data, target, path=''):
    results = []
    if isinstance(data, dict):
        for k, v in data.items():
            new_path = f'{path}.{k}' if path else k
            results.extend(find_value(v, target, new_path))
    elif isinstance(data, list):
        for idx, item in enumerate(data):
            new_path = f'{path}[{idx}]'
            results.extend(find_value(item, target, new_path))
    else:
        if data == target:
            results.append(path)
    return results

data = {'a': 1, 'b': [1, 2, {'c': 1}]}
print(find_value(data, 1))  # ['a', 'b[0]', 'b[2].c']
['a', 'b[0]', 'b[2].c']

Этот подход легко адаптировать для поиска по условию (например, все числа больше 10).

Вложенные структуры данных в Python - comments

En
вложенные структуры данных в python (python)