Меня зовут Стас Гаранжа, я выпускник курса «Python-разработчик» в Яндекс.Практикуме. Я хочу помочь начинающим разработчикам, которые приступили к изучению Django Rest Framework (DRF) и хотят разобраться, как устроен этот фреймворк. Я готовлю цикл статей, в которых расскажу о разных сторонах работы DRF. У меня пока нет значимого практического опыта для описания всех изюминок при работе с этим фреймворком, поэтому в основе статьи — исследование, обобщение и по возможности непротиворечивое изложение того, что у DRF под капотом. В этой статье разберёмся, как сделать REST API на базе Django Rest Framework, чтобы получить по GET-запросу набор записей из базы данных (БД). Иными словами, рассмотрим, как DRF работает на чтение (о том, как с помощью него создавать, изменять и удалять записи в БД, поговорим в отдельной статье).  Общую схему решения этой задачи мы рассмотрим в первой части статьи. Вторая будет посвящена детальному разбору процесса сериализации данных.  Несколько вводных замечаний: Надеюсь, статья станет хорошим подспорьем изучения DRF и работы с его документацией, прояснит процесс сериализации данных и даст уверенность, что любая магия исчезает, стоит только покопаться под капотом конкретной библиотеки. На локальном сервере работает одностраничный сайт на Django. На единственной странице сайта по адресу http://localhost:8000 пользователи видят информацию о четырёх североевропейских столицах. Информация попадает на страницу из подключённой к сайту базы данных, в которой есть модель Capital с пятью полями: Поле author через внешний ключ (foreign key) связано с моделью User, в которой есть вся информация о пользователе с конкретным id. Мы хотим получить информацию из базы данных, не открывая сайт в браузере, а сделав запрос из другого Python-приложения. В каком виде нужно получить информацию: Таким образом, каждую запись, которая при извлечении из базы данных является Python-объектом, принимающее приложение после декодирования json-строки должно получать в виде словаря: В этом и состоит одно из назначений API — дать возможность различным приложениям доставать из БД сайта информацию в виде структуры данных, которую дальше можно обрабатывать. Задача решается в два шага: Небольшое отступление о json. Базовые структуры данных на python кодируются в json и декодируются обратно следующим образом: Каждая запись в таблице Capital — объект. И как у любого объекта, у записи есть свои атрибуты. Изучим их на примере первой записи о столице Норвегии, воспользовавшись атрибутом __dict__. Нам доступен словарь, который хранит информацию о динамических (writable) атрибутах объекта: Каждое поле модели Capital — атрибут объекта конкретной записи. При этом поле author, которое через внешний ключ связано с моделью User и содержит id объектов из неё, в атрибуте записи и в БД получает приставку _id. Сериалайзер поможет достать данные из нужных атрибутов (полей) записи и сформировать упорядоченный python-словарь — объект класса OrderedDict. Отмечу, что в Python с версии 3.7 и «обычные» словари стали сохранять порядок вставки пар «ключ — значение». Для сериалайзера нужно описать поля: каждое поле будет отвечать за извлечение и представление данных из корреспондирующего поля табличной записи. Важный момент: здесь мы рассматриваем сериалайзер на основе базового класса Serializer, чтобы лучше понять принципы его работы. На более высоком уровне абстракции есть класс ModelSerializer, который позволяет частично уйти от ручного создания полей. В этой статье он не рассматривается. Нас интересуют данные, которые есть в трёх полях каждой табличной записи: Значит, в сериалайзере должно быть тоже три атрибута-поля. При создании поля сериалайзера нужно определиться с названием поля и его типом. Назвать поля сериалайзера можно как угодно: именно эти названия будут ключами в словаре, в который сериалайзер преобразует запись из таблицы.  Вот примеры трёх вариантов названий полей сериалайзера:  Но как сериалайзер понимает, в каком порядке стыковать собственные поля с полями табличной записи? Например, если поле сериалайзера условно называется a, то как он определяет, что его нужно состыковать с полем записи capital_city? Логика такая: Теперь нужно выбрать тип поля сериалайзера. Его нужно соотнести с тем, какие данные извлекаются из корреспондирующего поля табличной записи. Дело в том, что у каждого поля сериалайзера есть собственный метод to_representation. Как следует из названия, задача метода — представить извлечённые из записи данные в определённом виде. Например, есть поле serializers.IntegerField. Посмотрим на его метод to_representation: Очевидно, этот тип поля сериалайзера нельзя выбирать для данных из табличной записи о названии столицы: int('Осло') вызовет ValueError. А вот для данных о численности населения — самое то. Выберем следующие типы полей сериалайзера: О соотношении полей сериалайзера и полей Django-моделей можно прочитать в документации DRF. Код сериалайзера разместим в том же приложении, где находится Django-модель, под именем serializers.py: В поле CharField указан необязательный параметр max_length, благодаря которому задаётся максимально допустимая длина передаваемого значения. О других параметрах поля написано в документации. Для полей сериалайзера capital_city и capital_population мы не передаём аргумент source — названия поля сериалайзера и корреспондирующего поля табличной записи совпадают. Для поля author, наоборот, нужен аргумент source. В поле author модели Capital есть только id автора, а нам нужен его username. За этим значением мы идём в таблицу с данными о пользователях, с которой поле author связано по внешнему ключу. Используем точечную нотацию author.username. Пропущенный через сериалайзер набор табличных записей доступен в атрибуте сериалайзера data. Посмотрим на содержимое этого атрибута, создав тестовый вариант сериалайзера. Обратимся к файлу serializer_example_1.py. Он имитирует работу сериалайзера без необходимости запускать сервер и делать запрос к сайту. После клонирования учебного проекта и установки зависимостей (шаги 1—6 из ридми) достаточно запустить файл как обычный Python-скрипт и посмотреть в консоли результат его работы. В serializer_example_1.py созданы классы с данными об авторах и о столицах для записей в таблицах: Созданы объекты соответствующих записей: Объединены записи в список по подобию кверисета из Django-модели: Объявлен класс сериалайзера: код идентичен тому, который был приведён выше для class CapitalSerializer(serializers.Serializer). Затем создали его экземпляр: При создании мы передали сериалайзеру набор записей, которые нужно преобразовать. Они передаются в аргументе instance. Кроме того, мы указали аргумент many со значением True. Дело в том, что логика работы сериалайзера с одной записью и с набором записей разная. Указывая many=True, мы включаем логику обработки набора записей. В чём она заключается, расскажу во второй части статьи при детальном разборе работы сериалайзера. Выведем в консоль содержимое атрибута data сериалайзера: Каждая запись из набора превратилась в упорядоченный словарь класса OrderedDict. Он находится в Python-модуле collections. Поэтому, если взглянуть на строки импорта в исходном коде restframework.serializers, можно увидеть:  В каждом OrderedDict содержится информация только из тех полей табличных записей, которые были состыкованы с полями сериалайзера. Данных о содержимом поля country нет — сериалайзер не настроен доставать эту информацию, потому что мы не создавали корреспондирующего поля в сериалайзере. Нам понадобится рендер — объект класса JSONRenderer. В файле serializer_example_2.py мы дополнили импорт — помимо модуля сериалайзеров из restframework мы импортировали модуль рендеров. Далее необходимо создать экземпляр рендера нужного типа и вызвать у него метод render: В результате мы увидим байтовую строку с массивом json-объектов: Эта байтовая строка и будет передаваться по сети в атрибуте ответа content, а принимающее приложение будет её декодировать в список из Python-словарей и вытаскивать нужную информацию из каждого. Итак, мы испытали сериалайзер и посмотрели, как пропущенный через него набор табличных записей был преобразован в json-формат. Чтобы сайт начал отдавать сериализованные данные, остаётся описать контроллер (view) и указать url-маршрут — эндпоинт, при обращении к которому сайт будет отдавать данные о столичных городах. Во views.py создадим класс контроллера. Нам понадобятся следующие инструменты DRF: Внутри контроллера описываем один метод — get. Почему он называется именно так? Логика класса-родителя APIView, а значит, и класса контроллера, такова: в контроллере запускается метод, чьё имя совпадает с именем метода поступившего http-запроса в нижнем регистре. Ровно так же работает родительский View-класс в Django. Пример: если поступил GET-запрос, то будет задействован метод get контроллера. В методе get опишем ту же логику, что и в файле с пробным запуском сериалайзера: В отличие от файла serializer_example_2.py, где мы явно прописывали json-рендер и вызывали у него метод render, в коде контроллера ничего такого нет. Но рендер всё равно отработает: его работа описана под капотом внутри класса-родителя APIView. После того как отработал метод get, работа контроллера выглядит так: Благодаря этим атрибутам формируется rendered_content: у экземпляра JSONRenderer срабатывает метод render, который возвращает байтовую строку с данными в json-формат. Она помещается в атрибут ответа content. Здесь та же схема действий, как в классическом Django. Подключаем маршруты приложения capitals: Прописываем сам маршрут в приложении capitals и связываем маршрут с контроллером: Чтобы посмотреть, как работает API, можно: Первый шаг уже сделан: в корне учебного проекта есть файл get_info_from_our_site.py. Этот скрипт делает запрос к http://localhost:8000/api/capitals/, декодирует полученный json-ответ и записывает информацию о столицах и их населении в текстовый файл. Осталось выполнить шаги 2 и 3. Если всё отработало штатно, в корневой директории проекта появится файл capitals.txt со следующим содержимым: Несмотря на то, что пример наивный, он показывает главное: как мы научили
веб-приложение отдавать информацию из базы данных в ответ на запрос, который поступает не от человека через браузер, а от другого приложения. И далее — как это приложение использует полученную информацию. Django Rest Framework позволяет посмотреть в браузере, какую информацию будет отдавать API при обращении к конкретному маршруту (эндпоинту). Достаточно ввести маршрут в адресную строку, и откроется страница с данными о запросе и результате его выполнения. За такое отображение отвечает BrowsableAPIRenderer.  Итак, мы рассмотрели, как сделать API на базе DRF, чтобы получить по GET-запросу набор записей из Django-модели. Во второй части подробно разберём работу сериалайзера на чтение.
Если у вас появились вопросы по решению задачи, пишите в комментариях.