Как только задача становится хоть немного похожей на реальность, «чистый список» перестаёт хватать. В мире есть товары, у товаров есть свойства, у заказов есть позиции, у позиций — количество. Это уже не плоские данные, а структура из структур.

Разберём это на одном примере: модель каталога товаров и заказов в памяти.
Мы:

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

Один пример на всю статью.

Chatgpt image 5 дек. 2025 г., 17 25 54

Сначала пишем код

Есть маленький каталог и список заказов. Нужна программа, которая печатает чек по заказу: что купили и на какую сумму.

Начнём с простого представления:

  • каталог — словарь, где ключ это id товара, а значение — словарь свойств товара;
  • заказ — словарь с id заказа и списком купленных позиций;
  • позиция — пара: product_id и qty.
catalog = {
    "p100": {"name": "Keyboard", "price": 49.9, "category": "electronics"},
    "p200": {"name": "Mouse", "price": 19.9, "category": "electronics"},
    "p300": {"name": "Notebook", "price": 3.5, "category": "stationery"},
}

orders = [
    {
        "order_id": "o9001",
        "items": [
            {"product_id": "p100", "qty": 1},
            {"product_id": "p300", "qty": 4},
        ]
    },
    {
        "order_id": "o9002",
        "items": [
            {"product_id": "p200", "qty": 2},
        ]
    }
]

Теперь печатаем чек для первого заказа.

order = orders[0]
total = 0

print("Order:", order["order_id"])

for item in order["items"]:
    product = catalog[item["product_id"]]
    line_sum = product["price"] * item["qty"]
    total += line_sum

    print(product["name"], "x", item["qty"], "=", round(line_sum, 2))

print("Total:", round(total, 2))

Эта версия уже делает полезную вещь: по данным в памяти строит чек.
Теперь понятно, зачем вообще нужна вложенность.

Разбираем, как это устроено

Почему каталог — словарь словарей

catalog = {
    "p100": {"name": "...", "price": ..., "category": "..."},
    ...
}

Смысл:

  • внешний словарь даёт быстрый доступ по product_id;
  • внутренний словарь хранит свойства товара по именам.

Если бы каталог был списком, поиск товара по id выглядел бы как цикл с перебором. А словарь позволяет получить товар сразу:

product = catalog[item["product_id"]]

То есть у нас dict для индекса, а внутри снова dict для удобного доступа к полям.

Почему заказ — это словарь со списком позиций

{
    "order_id": "o9001",
    "items": [ ... ]
}

items — список, потому что:

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

Почему позиция — словарь

{"product_id": "p100", "qty": 1}

Это «микро-объект» внутри списка.
Словарь здесь выигрывает читаемостью: item["qty"] сразу понятно, что это количество. Если бы это был список [product_id, qty], пришлось бы помнить индексы.

Как читать вложенные структуры

В нашем чеке есть цепочка доступа:

  1. получаем заказ из списка:
    order = orders[0]
  2. достаём список позиций:
    for item in order["items"]:
  3. по product_id находим товар в каталоге:
    product = catalog[item["product_id"]]
  4. достаём поля товара и позиции:
    product["price"], product["name"], item["qty"]

Это и есть нормальный способ работы с «структурами реального мира»:
список контейнеров → контейнеры внутри контейнеров → доступ по ключам.

Улучшаем и расширяем

Функции разберем в следующей главе.

Сейчас наш код для чека — голая логика «на месте». Сделаем две вещи:

  • вынесем создание чека в функцию;
  • добавим полезные операции поверх той же модели данных.

Чек как функция

def print_receipt(order, catalog):
    total = 0
    print("Order:", order["order_id"])

    for item in order["items"]:
        product = catalog[item["product_id"]]
        line_sum = product["price"] * item["qty"]
        total += line_sum
        print(product["name"], "x", item["qty"], "=", round(line_sum, 2))

    print("Total:", round(total, 2))
    return total

📢 Подписывайтесь на наш Telegram-канал.

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

👉 https://t.me/codelab_channel

Использование:

print_receipt(orders[0], catalog)

Модель данных не изменилась. Мы просто сделали работу с ней чище.

Операция «общая выручка»

Раз данные уже структурированы, можно легко считать:

def total_revenue(orders, catalog):
    revenue = 0
    for order in orders:
        for item in order["items"]:
            product = catalog[item["product_id"]]
            revenue += product["price"] * item["qty"]
    return revenue

print("Revenue:", round(total_revenue(orders, catalog), 2))

Снова один и тот же доступ к вложенным структурам, только цель другая.

Операция «товары по категориям»

Хотим получить все товары одной категории, например электронику.

def products_by_category(catalog, category):
    result = []
    for product_id, product in catalog.items():
        if product["category"] == category:
            result.append(product_id)
    return result

print(products_by_category(catalog, "electronics"))

Две важные детали:

  • catalog.items() даёт пары (product_id, product) — удобно для фильтрации.
  • результатом делает список id, потому что порядок нам здесь не критичен, а хранить просто.

Делаем модель чуть прочнее

Сейчас items — список словарей. Это гибко, но есть риск опечатки в ключе. Мы можем стандартизировать ключи и добавить проверку.

Например, функция добавления позиции в заказ:

def add_item(order, product_id, qty):
    if qty <= 0:
        raise ValueError("qty must be positive")
    order["items"].append({"product_id": product_id, "qty": qty})

Теперь вы не пишете вручную словарь позиции каждый раз, а пользуетесь «строителем». Это повышает надёжность модели, не меняя её сути.

Почему это читаемо и масштабируется

Мы не пытались заранее придумать «идеальную архитектуру».
Мы взяли реальную структуру:

  • каталог → товары
  • заказ → позиции
  • позиция → ссылка на товар + количество

и построили её минимально адекватным способом.

Серьёзность тут в том, что:

  • dict даёт семантические имена полям;
  • list даёт естественное представление «много элементов»;
  • вложенность повторяет то, как вы мысленно представляете задачу.

Если модель совпадает с головой — код читается.

Комбинирование структур данных — это не сложность, а способ сохранить ясность. Когда вы видите catalog[item["product_id"]]["price"], вы точно понимаете путь от позиции в заказе к цене товара.

Практика на том же примере

Никаких новых сюжетов. Развиваем эту же модель.

  1. Добавьте поле status у заказа ("new", "paid", "shipped").
  2. Напишите функцию, которая возвращает только заказы со статусом "paid".
  3. Сделайте функцию order_total(order, catalog), которая возвращает сумму заказа, не печатая чек.
  4. Добавьте скидку на категорию: функция принимает категорию и процент скидки, и возвращает выручку с учётом скидки только на товары этой категории.

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