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

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

Начать обучение
Урок 17 из 20 Начальный 20 мин 130 XP

Словарь как данные: разбор ответа API

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

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

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

Как выглядит ответ API

ответ погодного сервиса
response = {
    'city': 'Сочи',
    'temp': 24,
    'wind': 3,
    'details': {'humidity': 71, 'pressure': 755},
}

print(response['city'])
print(response['details']['humidity'])
Вывод
Сочи
71

Перед вами обычный словарь — просто с пополнением: под ключом 'details' лежит ещё один словарь, знакомый по уроку про вложенные словари. Верхний этаж — сводка: город, температура, ветер. Нижний — детали: влажность и давление. Строка response['details']['humidity'] читается за два шага: сначала из response достали вложенный словарь, потом из него — число.

Вложенный доступ: цепочка ключей

поля о пользователе
response = {
    'status': 'ok',
    'user': {'name': 'Анна', 'friends': 42},
}

name = response['user']['name']
friends = response['user']['friends']
print(name, friends)
Вывод
Анна 42

get-цепочка: чтение без падений

Квадратные скобки честны, но требовательны: не будет ни 'user', ни 'email' — программа упадёт с KeyError, как и положено. А в ответах API отсутствие поля — норма: сервис не у всех заполняет почту, старые версии присылают меньше полей. Метод get из урока про безопасное чтение выстраивается в цепочку. Каждый шаг цепочки достаёт вложенный словарь или подставляет пустой — и потому цепочка не падает даже на неполных данных.

чтение полей, которых может не быть
response = {'status': 'ok', 'user': {'name': 'Анна'}}

email = response.get('user', {}).get('email', 'почты нет')
print(email)

city = response.get('geo', {}).get('city', 'город неизвестен')
print(city)
Вывод
почты нет
город неизвестен

Извлечение полей: отчёт из ответа

поля — в переменные, переменные — в отчёт
response = {'city': 'Сочи', 'temp': 24, 'details': {'humidity': 71}}

city = response.get('city', '—')
temp = response.get('temp', 0)
feels = response.get('feels_like', temp)
humidity = response.get('details', {}).get('humidity', '—')

print(f'{city}: {temp} градусов, ощущается как {feels}, влажность {humidity}%')
Вывод
Сочи: 24 градусов, ощущается как 24, влажность 71%

Когда в ответе лежит список

Погодный ответ — это словарь с полями. Ответ интернет-магазина или логи сервиса устроен иначе: там в словаре лежит список словарей, и разбирать его приходится в два шага. Верхний уровень даёт метаданные, а полезная начинка — под ключом items, results или data, и имя это у каждого сервиса своё.

список словарей внутри ответа
response = {
    'status': 'ok',
    'data': {'items': [{'id': 1, 'title': 'Хлеб'}, {'id': 2, 'title': 'Сыр'}]},
}

items = response.get('data', {}).get('items', [])

print(len(items))
print(items[0]['title'])
print([i['title'] for i in items])
Вывод
2
Хлеб
['Хлеб', 'Сыр']

Цепочка из двух get с пустым словарём в умолчании разбирает два уровня вложенности. Дальше items — обычный список, и с ним работают все приёмы из урока про перебор: len(items) даёт количество записей, items[0]['title'] достаёт поле первой, а list comprehension собирает список названий. Отдельно заметьте: get('items', []) возвращает пустой список, если поля нет, и код после него работает без проверок — цикл по пустому списку просто не выполнится.

Списки внутри ответа приходят с фильтрами и сортировкой, и это отдельная работа. Отбор нужных записей делается обычным comprehension по элементам списка:

отбор дорогих позиций из ответа
response = {'items': [{'id': 1, 'price': 90},
                                {'id': 2, 'price': 250},
                                {'id': 3, 'price': 40}]}

expensive = [i for i in response.get('items', []) if i.get('price', 0) > 100]

print(expensive)
print([i['id'] for i in expensive])
Вывод
[{'id': 2, 'price': 250}]
[2]

Обратите внимание на двойную защиту: response.get('items', []) спасает от отсутствующего ключа на верхнем уровне, а i.get('price', 0) — от отсутствующего поля в каждой записи. Если бы в какой-то записи поля price не оказалось, программа упала бы с KeyError ровно на середине обработки, когда часть данных уже применена. Правило get с нулём внутри фильтра — стандартная страховка при работе с чужими данными.

Опасное молчание: get возвращает None

Вот место, где get предаёт. Если ключ в словаре есть, но значение равно None, get вернёт именно None, а не запасное значение по умолчанию. Программа не упадёт — она продолжит работать с None вместо значения, и ошибка вылезет позже и совсем в другом месте.

ключ есть, но значения нет
resp = {'user': {'name': None, 'email': 'a@b.c'}}

print(resp.get('user', {}).get('name', 'нет имени'))
print(resp.get('user', {}).get('name'))
print(resp.get('user', {}).get('name') is None)
Вывод
None
None
True

Первый print удивит: мы просили 'нет имени', а получили None. Умолчание в get работает только когда ключа нет в словаре. Здесь ключ 'name' есть — просто под ним лежит None, и сервер честно сообщил «значение отсутствует» отдельным словом от «поля нет». Разница видна только во втором print и в проверке is None: значение действительно None, а не «отсутствующее поле».

falsy-значения ловят не только None
d = {'price': 0, 'title': '', 'tags': []}

print(d.get('price', 100))
print(d.get('title', 'нет'))
print(d.get('tags', ['-']))
Вывод
0

[]

Здесь умолчание не сработало ни разу: все три ключа существуют, просто значения «пустые» с точки зрения Python. price равно 0 — и print показал 0, а не 100. title — пустая строка, она напечаталась пустой строкой, а не словом 'нет'. tags — пустой список, а не ['-']. Это важное отличие get от проверки if not value: пустой список и ноль — законные значения, и заменять их на «умолчание» нельзя. Умолчание в get означает «поля нет», а не «значение пустое».

Группировка записей ответа

Ответ с логами или списком заказов почти всегда нужно сгруппировать: по пользователю, по статусу, по дню. Данные приходят плоским списком словарей, а нужна структура «ключ — список записей». Здесь снова спасает defaultdict из урока 12: список под ключом создаётся сам, без проверок.

логи, сгруппированные по пользователю
from collections import defaultdict

log = [
    {'user': 'anna', 'action': 'login'},
    {'user': 'anna', 'action': 'buy'},
    {'user': 'bob', 'action': 'login'},
]

by_user = defaultdict(list)
for entry in log:
    by_user[entry['user']].append(entry['action'])

print(dict(by_user))
Вывод
{'anna': ['login', 'buy'], 'bob': ['login']}

Разбор по шагам. by_user[entry['user']] — обращение к ключу, которого, возможно, ещё нет; defaultdict(list) в этот момент создаёт под ним пустой список. Дальше .append(...) кладёт действие в этот список. На выходе — словарь, где по имени пользователя лежит список его действий. Тот же результат даст setdefault из урока 10, просто строкой длиннее. В конце печатаем dict(by_user), а не сам defaultdict: обычный словарь не создаст пустой список при обращении к отсутствующему ключу.

Сводка по ответу: от данных к отчёту

Последний шаг разбора — превратить разобранное в то, что видит человек. Данные лежат в словаре, у отчёта свой формат, и одно поле ответа не всегда равно тому, что должно быть в строке. Собирать отчёт удобно через словарь, а не через длинный f-string: строки собираются отдельно, и формат потом можно поменять в одном месте.

отчёт собирается словарём
response = {'city': 'Сочи', 'temp': 24, 'wind': 3, 'details': {'humidity': 71}}

summary = {
    'город': response.get('city', '—'),
    'температура': response.get('temp', 0),
    'влажность': response.get('details', {}).get('humidity', '—'),
}

for key, value in summary.items():
    print(f'{key}: {value}')
Вывод
город: Сочи
температура: 24
влажность: 71

Обратите внимание на раскладку умолчаний. Для города и температуры это прочерк и ноль — то есть «сервис не прислал, покажи заглушку». Для влажности это снова прочерк, но достаётся он из вложенного словаря, поэтому цепочка длиннее на один шаг с {}. Словарь summary потом можно отдать в шаблон, в JSON или в таблицу — и менять формат вывода в одном месте, а не переписывать f-строки по всему коду.

Что дальше

Разбор ответа разобран. Дальше — вопрос, что можно положить в ключ такого словаря и что нельзя: в уроке 18 разбираем хешируемость, TypeError на списке в ключе и frozenset для наборов. А если с ответом нужно сравниться или его надо скопировать перед правкой — урок 19 про ==, copy и deepcopy.

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

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

d = {'a': {'b': 7}}

print(d['a']['b'])
d = {}

print(d.get('a', {}).get('b', 0))
d = {'x': 5}

print(d.get('x', 9))
Проверь себя
0 / 8

1. Что такое ответ API с точки зрения Python?

2. Как достать humidity из response, если под 'details' лежит вложенный словарь?

3. Зачем в get-цепочке пустой словарь: response.get('details', {})?

4. Чем опасна запись response.get('user').get('email') без умолчания?

5. Почему в разборе ответа API предпочитают get со строгими скобками?

6. Что вернёт resp.get('user', {}).get('name', 'нет имени'), если в ответе name равно None?

7. Как безопасно достать список записей из ответа, где он может отсутствовать?

8. Как сгруппировать список записей ответа по полю user?

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

Дан ответ сервиса с вложенным словарём user. Достань через get-цепочку имя (умолчание 'нет имени'), город (умолчание 'город неизвестен') и почту (умолчание 'почты нет') — почты в ответе нет. Выведи три значения, каждое на своей строке.

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

Как достать значение из вложенного словаря в Python?

Цепочкой доступов: response['details']['humidity'] — сначала достаётся вложенный словарь, из него поле. Если поле может отсутствовать, замените доступы на get с умолчаниями: response.get('details', {}).get('humidity', '—') — такая цепочка не падает даже на неполном ответе.

Урок 4: метод get

Почему response.get('user').get('email') падает с AttributeError?

Потому что без умолчания первый get вернёт None, когда ключа 'user' нет, а у None нет метода get. Пишите пустой словарь вторым аргументом: response.get('user', {}) — тогда следующий шаг найдёт свой get и вернёт своё умолчание.

Как разобрать JSON-ответ из requests?

У ответа requests есть метод .json(): data = r.json() сразу возвращает словарь. Дальше тот же разбор, что в этом уроке: поля верхнего уровня, get-цепочки для вложенных, умолчания для необязательных полей. Пример с настоящим HTTP — в разделе про requests.

Раздел: requests

Что подставлять в умолчание для числовых полей?

Значение, с которым отчёт останется осмысленным: 0 для сумм, прочерк для показа человеку, а иногда значение соседнего поля — как feels = response.get('feels_like', temp), где при отсутствии ощущаемой температуры подставляется обычная.

Как разобрать ответ, где под ключом лежит список словарей?

Цепочкой get с пустым списком в умолчании: response.get('data', {}).get('items', []). Дальше это обычный список — len(items) даст количество, comprehension соберёт нужные поля. Если списка нет, вернётся пустой список, и код отработает без падения.

Почему get не вернул умолчание, если значение равно None?

Умолчание в get означает «поля нет», а срабатывает оно только при отсутствующем ключе. Если ключ есть, а под ним лежит None, get вернёт именно None. Отличить одно от другого можно проверкой is None — а если None недопустим, приводите значение к нулю вручную.

Как сгруппировать записи ответа по пользователю?

Через defaultdict(list): по ключу создаётся список, в который кладутся значения из каждой записи. Тот же результат даст setdefault в цикле, строкой длиннее. В конце полезно печатать dict(...), а не сам defaultdict — иначе случайное обращение к отсутствующему ключу создаст лишний пустой список.

Урок 10: setdefault

Как защититься от опечатки в ключе ответа?

Цепочка с get никогда не падает, поэтому опечатка в ключе не выдаст себя — программа молча покажет умолчание. Проверяйте критичные поля явно: assert response.get('city'), 'в ответе нет города'. Для данных извне такая проверка обязательна.

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

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

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

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

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

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