Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 15 из 20 Средний 35 мин 110 XP

Фильтрация ответа API: цены, рейтинги, флаги

Товары из ответа API приехали — теперь вопросы: что дешевле N, что в наличии, какая средняя цена и сколько процентов подходит под условие.

Редакция Питоники

Ответ API разобран json.loads — в прошлом уроке список товаров уже приехал в переменную items. Но сам по себе этот список бесполезен: никто не читает тысячи позиций каталога подряд. Настоящая работа начинается, когда по данным задают вопросы: что дешевле трёх тысяч, что есть в наличии, какая средняя цена, сколько вообще товаров подходит под условие.

Хорошая новость: на все эти вопросы отвечает стандартный Python — условия, списки включения и пара встроенных функций. Ставить ничего не нужно, и каждый пример ниже исполняется прямо на странице, как и весь курс.

Ответ, с которым работаем

Сети в браузерной песочнице нет, поэтому текст ответа лежит в константе — ровно в том виде, в каком его отдал бы настоящий сервер. Разбор от этого не меняется: json.loads превращает текст в словарь, дальше работаем с обычным списком словарей.

разбираем сохранённый ответ
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

data = json.loads(API_RESPONSE)
items = data["items"]

print(len(items), "товара в ответе")
print(items[0]["name"], "-", items[0]["price"], "руб.")
Вывод
5 товара в ответе
Клавиатура - 4990 руб.

Пять товаров — пять словарей с одинаковым набором полей: name, price, rating и флаг in_stock. Однообразие структуры — подарок: одно и то же условие можно применить к каждому элементу скопом, без единого ручного if.

Фильтр по цене: условие вместо ручного перебора

Фильтр по ответу API — это обычное условие if, применённое сразу ко всему списку: прошёл товар по условию, попал в выборку. Задача «выбрать дешевле 5000» решается списком включения: обходим items, оставляем только прошедших проверку. Синтаксис тот же, что в уроке про фильтрацию, — меняется только источник данных.

только дешевле 5000
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

cheap = [item for item in items if item["price"] < 5000]

for item in cheap:
    print(item["name"], "-", item["price"])
print("Подходящих:", len(cheap))
Вывод
Клавиатура - 4990
Мышь - 1290
Веб-камера - 2450
Подходящих: 3

В выборке остались только словари, для которых условие оказалось истинным. Исходный items при этом не изменился: фильтр строит новый список, а не правит старый — это важно, когда те же данные нужны для другого вопроса.

Заодно заметь, какие типы приехали в полях: цена — целое число, рейтинг — дробное. Это соответствие типов из урока 3: JSON-число без точки становится int, с точкой — float. Сравнения вроде < и >= требуют именно чисел — и когда сервер их отдал честно, условие работает без подготовки. А вот true и false из JSON — это True и False, и с ними фильтр работает чуть иначе, о чём ниже.

два условия сразу
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

found = [item for item in items
         if item["price"] < 8000 and item["rating"] >= 4.0]

for item in found:
    print(item["name"], "-", item["price"], "- рейтинг", item["rating"])
Вывод
Клавиатура - 4990 - рейтинг 4.7
Мышь - 1290 - рейтинг 4.3
Веб-камера - 2450 - рейтинг 4.1

Условий может быть сколько угодно: and требует выполнения обоих, or — хотя бы одного. Так собираются настоящие выборки «дешевле 8000 и рейтинг не ниже 4»: Наушники прошли по цене, но не прошли по рейтингу — и остались за бортом.

так падает фильтр по строковой цене
items_v2 = [
    {"name": "Мышь", "price": "1290"},   # цена приехала строкой
    {"name": "Коврик", "price": "890"},
]

cheap = [item for item in items_v2 if item["price"] < 1000]
# TypeError: '<' not supported between instances of 'str' and 'int'
Блок не запускается намеренно: этот код действительно падает с TypeError. Это честное поведение Python, а не ограничение песочницы — перед сравнением цену надо привести к числу.
float() чинит сравнение
items_v2 = [
    {"name": "Мышь", "price": "1290"},
    {"name": "Коврик", "price": "890"},
]

cheap = [item for item in items_v2 if float(item["price"]) < 1000]

print(cheap[0]["name"], "-", cheap[0]["price"], "руб.")
Вывод
Коврик - 890 руб.

float() превратил строку в число — и условие заработало. Заметь: в выборке цена осталась строкой «890», фильтр ничего не переписывает, он только отбирает. Если дальше с ценой нужно считать — приводи её к числу отдельно, например списком цен [float(i["price"]) for i in cheap].

Флаг in_stock: фильтр по булеву значению

Булевы поля из JSON приезжают настоящими bool: true становится True, false — False. Поэтому условие «только в наличии» — само значение поля, без всяких сравнений.

только то, что в наличии
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

available = [item for item in items if item["in_stock"]]

for item in available:
    print(item["name"], "-", item["price"])
print(len(available), "из", len(items), "в наличии")
Вывод
Клавиатура - 4990
Мышь - 1290
Наушники - 7350
3 из 5 в наличии

Написать item["in_stock"] == True можно — результат тот же, но сравнение с True лишнее: значение уже булево, и Python понимает его напрямую. «Нет в наличии» — это not item["in_stock"], а комбинация «в наличии и недорого» собирается через and, как в прошлом разделе. Составные условия поверх флагов — половина реальных выборок: [i for i in items if i["in_stock"] and i["rating"] >= 4.5] — уже готовая витрина «лучшее из доступного».

Среднее по полю: sum и len

Средняя цена — это сумма всех значений поля, делённая на их количество. Список включения собирает цены, sum() складывает, len() считает. Настоящие вопросы к данным почти всегда такие: не «покажи всё», а «сколько в среднем».

средняя цена по выборке
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

prices = [item["price"] for item in items]
avg = sum(prices) / len(prices)
print("Средняя цена:", round(avg, 2))

available = [item for item in items if item["in_stock"]]
avg_stock = sum(i["price"] for i in available) / len(available)
print("Средняя цена в наличии:", round(avg_stock, 2))
Вывод
Средняя цена: 6896.0
Средняя цена в наличии: 4543.33

round(avg, 2) оставляет два знака после запятой — для отчёта это то, что нужно: без округления вторая строка напечаталась бы хвостом 4543.333333333333. Обрати внимание на второй подсчёт: генератор i["price"] for i in available экономит отдельный список, когда цены нужны только для суммы.

Доля подходящих

Единицы отчёта — не только рубли, но и проценты: какая доля товаров прошла фильтр. Считается теми же len(): подходящих делим на всех и умножаем на сто.

сколько процентов прошло фильтр
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

cheap = [item for item in items if item["price"] < 5000]
share = len(cheap) / len(items) * 100

print("Дешёвых:", len(cheap), "из", len(items))
print("Доля:", round(share), "процентов")
Вывод
Дешёвых: 3 из 5
Доля: 60 процентов

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

Лидер по полю: max и min

Крайние значения — самый дорогой и самый дешёвый товар — достаются без сортировки: max() и min() с параметром key сами найдут лидера по полю.

товар-лидер по цене
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

priciest = max(items, key=lambda item: item["price"])
cheapest = min(items, key=lambda item: item["price"])

print("Дороже всех:", priciest["name"], "-", priciest["price"])
print("Дешевле всех:", cheapest["name"], "-", cheapest["price"])
Вывод
Дороже всех: Монитор - 18400
Дешевле всех: Мышь - 1290

max() с key вернул целый словарь — товар-лидер, а не число: key говорит функции, по какому полю сравнивать. Полный топ-N по местам строит функция sorted — её разбор ждёт в следующем уроке.

Пустая выборка: страховка от деления на ноль

среднее с проверкой
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

expensive = [item for item in items if item["price"] > 100000]
print("Найдено:", len(expensive))

if expensive:
    avg = sum(i["price"] for i in expensive) / len(expensive)
    print("Средняя цена:", avg)
else:
    print("Пусто: считать среднее не из чего")
Вывод
Найдено: 0
Пусто: считать среднее не из чего

Проверка if expensive: запускает подсчёт только для непустого списка — тот же приём защиты, что try/except вокруг битого JSON в уроке 9, только здесь она бережёт арифметику, а не разбор.

Сводка по каталогу: все приёмы в пяти строках

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

отчёт по каталогу целиком
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]
cheap = [i for i in items if i["price"] < 5000]

summary = {
    "count": len(items),
    "cheap": len(cheap),
    "share": round(len(cheap) / len(items) * 100),
    "avg_price": round(sum(i["price"] for i in items) / len(items), 2),
    "best_rating": max(items, key=lambda i: i["rating"])["name"],
}

for key, value in summary.items():
    print(key, "=", value)
Вывод
count = 5
cheap = 3
share = 60
avg_price = 6896.0
best_rating = Монитор

Пять вопросов — пять вычислений, каждое в одну строку, и словарь summary готов стать отчётом. Цикл for key, value in summary.items() печатает его построчно: пара ключ-значение за проход — приём обхода словаря из урока 10. Такой сводке не нужна ни сортировка, ни файл данных целиком в отчёте — только числа, и всё они у тебя уже умеют считаться.

Вопрос к даннымПриём
что дешевле N[item for item in items if item["price"] < N]
рейтинг не ниже X и в наличииif item["rating"] >= X and item["in_stock"]
средняя ценаsum(цен) / len(цен)
доля подходящихlen(подходящих) / len(всех) * 100
самый дорогой товарmax(items, key=lambda i: i["price"])

Чек-лист обработки ответа

  • условие фильтра проверяет поле каждого словаря — записи должны быть одной структуры;
  • строковые цены приводи float() до сравнения, иначе TypeError;
  • булев флаг ставь в условие как есть: if item["in_stock"];
  • среднее и долю считай только после проверки, что выборка не пуста.
тот же фильтр после настоящего запроса
from urllib.request import urlopen
import json

url = "https://shop.example/api/products?max_price=5000"

with urlopen(url) as response:
    data = json.loads(response.read().decode("utf-8"))

cheap = [item for item in data["items"] if item["price"] < 5000]
print(len(cheap))
Единственная строка, которой нет в песочнице, — urlopen(url): сеть из браузера недоступна. На твоём компьютере блок работает как есть, а фильтр ниже идентичен исполняемым примерам: ответ приходит текстом, дальше — тот же loads и то же условие. Доставку ответов разбирает курс requests.

Такая конструкция — готовый каркас любого скрипта обработки: запрос, разбор, вопросы к данным. Меняется только URL и список условий — каркас переживает десятки задач: прайсы, выгрузки, отчёты по складу. Именно поэтому приёмы урока важнее конкретных цифр: цены и рейтинги условны, а условие в списке включения — нет.

Что дальше

Ты задал данным пять вопросов и получил на каждый ответ в пару строк: фильтр отобрал, sum и len посчитали, guard уберёг от пустой выборки. Дальше каталог хочется видеть по убыванию цены — сортировку и топ-N разбирает урок 16, а после неё результаты наконец можно записать на диск — этим займётся урок 17. Про сам запрос и доставку ответа подробно рассказывает курс requests.

Обработка ответа API — это вопросы к данным: условие выбирает, sum и len считают, а проверка на пустоту бережёт отчёт.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

items = [
    {"name": "A", "price": 100, "in_stock": True},
    {"name": "B", "price": 900, "in_stock": False},
    {"name": "C", "price": 500, "in_stock": True},
]

names = [i["name"] for i in items if i["price"] < 600 and i["in_stock"]]
print(len(names), names[0])
prices = [1200, 800, 2600]
print(round(sum(prices) / len(prices)))
items = [{"price": 100}, {"price": 300}]

cheap = [i for i in items if i["price"] > 500]
if cheap:
    print(len(cheap))
else:
    print("пусто")
Проверь себя
0 / 6

1. Как одним выражением выбрать из items все товары дешевле 3000?

2. Почему item["price"] < 1000 падает, если цена приехала строкой "1290"?

3. Что сделает фильтр [item for item in items if item["in_stock"]]

4. Как посчитать среднюю цену по списку цен prices?

5. Что вернёт max(items, key=lambda item: item["price"]) на списке товаров?

6. Зачем перед подсчётом среднего проверять выборку на непустоту?

Карточки терминов
Запомнено: 0 / 7
Практика

Из сохранённого ответа магазина выбери товары, которые есть в наличии и стоят дороже 1000 рублей. Выведи названия найденных товаров, затем среднюю цену выборки с подписью «Средняя:», округлённую до целого.

practice.py
Вопросы и ответы по уроку

Как отфильтровать список словарей из JSON по значению поля?

Списком включения с условием: [item for item in items if item["price"] < 5000]. Условие может проверять любое поле и комбинироваться через and и or. Исходный список при этом не меняется — фильтр возвращает новый. Полный набор приёмов фильтрации разобран в отдельном уроке.

Как посчитать среднее значение поля в JSON-данных?

Собери значения поля списком включения и раздели сумму на количество: sum(prices) / len(prices). Для красивого вывода округли round(avg, 2). Если выборка может оказаться пустой, сначала проверь её: деление на len пустого списка вызывает ZeroDivisionError.

Почему цена из API бывает строкой и что с ней делать?

JSON не требует типизировать числа: сервер вправе отдать "price": "1290". Строка с числом не сравнивается с int знаком «меньше» — будет TypeError. Приведи значение к числу перед сравнением: float(item["price"]) < 1000 или int(), если цены целые.

Чем разбор ответа API здесь отличается от работы с requests?

Ничем: requests доставляет ответ, а обработка начинается после него. response.json() в requests делает то же, что json.loads(response.text) — возвращает словарь. В этом курсе сеть заменена сохранённым текстом ответа, чтобы вся механика исполнялась прямо в браузере.

Что делать, если фильтр вернул пустой список?

Проверить выборку перед подсчётами: if found: — считать среднее и долю, else — вывести «ничего не найдено». Пустой список не ошибка, а честный ответ данных: товаров под условие просто нет. Падает не сам фильтр, а деление на len пустого списка в подсчётах после него.

Понравился урок? Сошлитесь на него

«Фильтр по ответу API — это обычное условие if, применённое сразу ко всему списку: прошёл товар по условию, попал в выборку.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.