FastAPI: веб-разработка на Python
Привет, Хаброжители!
FastAPI — относительно новый, но надежный фреймворк с чистым дизайном, использующий преимущества актуальных возможностей Python. Как следует из названия, FastAPI отличается высоким быстродействием и способен конкурировать в этом с аналогичными фреймворками на таких языках, как Golang. Эта практическая книга расскажет разработчикам, знакомым с Python, как FastAPI позволяет достичь большего за меньшее время и с меньшим количеством кода.
Билл Любанович рассказывает о тонкостях разработки с применением FastAPI и предлагает множество рекомендаций по таким темам, как формы, доступ к базам данных, графика, карты и многое другое, что поможет освоить основы и даже пойти дальше. Кроме того, вы познакомитесь с RESTful API, приемами валидации данных, авторизации и повышения производительности. Благодаря сходству с такими фреймворками, как Flask и Django, вы легко начнете работу с FastAPI.
Эта книга предназначена для опытных программистов, которые только начинают знакомиться с FastAPI. Книга дает исчерпывающее описание фреймворка FastAPI и окружающей его экосистемы, позволяя читателям быстро и полно ознакомиться с разработкой современных веб-приложений.
Часть II — это обзор FastAPI, свежего веб-фреймворка на Python. В этой части содержатся ответы на заданные в части I вопросы.
В части III мы углубляемся в инструментарий FastAPI, включая советы, полученные в процессе разработки.
Наконец, в части IV представлена галерея веб-примеров FastAPI. Для них использовался общий источник данных — список воображаемых существ, что может быть немного интереснее и целостнее, чем обычные случайные представления данных. Это должно дать вам отправную точку для конкретного применения этого веб-фреймворка.
Обзор
FastAPI во многом опирается на пакет Python с названием Pydantic. Для определения структур данных используются модели (объектные классы Python). Они широко применяются в приложениях FastAPI и становятся реальным преимуществом при написании больших приложений.
Подсказки типов данных
Пришло время узнать немного больше о подсказках типов в Python.
В главе 2 упоминалось, что во многих компьютерных языках переменная указывает непосредственно на значение в памяти. Это требует от программиста объявления типа значения, чтобы можно было определить его размер и разрядность. В Python переменные — это просто имена, связанные с объектами, и именно у объектов есть типы.
В стандартном программировании переменная обычно связана с одним и тем же объектом. Если мы свяжем с этой переменной подсказку типа, то сможем избежать некоторых ошибок в программировании. Поэтому Python добавил подсказки типов к языку, в стандартный модуль типизации. Интерпретатор Python игнорирует синтаксис подсказки типа и выполняет программу так, как будто ее нет. Тогда в чем смысл?
В одной строке вы можете рассматривать переменную как строку, а потом забыть и присвоить ей объект другого типа. Компиляторы других языков будут жаловаться, а Python этого не сделает. Стандартный интерпретатор Python отлавливает обычные синтаксические ошибки и исключения времени выполнения, но не смешивает типы переменных. Инструменты-помощники, такие как mypy, обращают внимание на подсказки типов и предупреждают о любых несоответствиях.
Кроме того, подсказки доступны разработчикам Python, которые могут написать инструменты, выполняющие не только проверку ошибок типов. В следующих разделах описывается, как пакет Pydantic был разработан для удовлетворения неочевидных потребностей. Позже вы увидите, как его интеграция с FastAPI значительно упрощает решение многих вопросов веб-разработки.
Кстати, как выглядят подсказки? Существует один синтаксис для переменных и другой — для возвращаемых значений функций.
Подсказки типа переменной могут включать только тип:
name: type
или также инициализировать переменную значением:
name: type = value
Тип может быть одним из стандартных простых типов Python, таких как int
илиstr
, или коллекцией, такой как tuple
, list
или dict
:
thing: str = "yeti"
При использовании Python до версии 3.9 необходимо импортировать прописные версии стандартных имен типов из модуля типизации:
from typing import Str
thing: Str = "yeti"
Вот несколько примеров с инициализацией:
physics_magic_number: float = 1.0/137.03599913
hp_lovecraft_noun: str = "ichor"
exploding_sheep: tuple = "sis", "boom", bah!"
responses: dict = {"Marco": "Polo", "answer": 42}
Можно также включать подтипы коллекций:
name: dict[keytype, valtype] = {key1: val1, key2: val2}
Модуль типизации содержит полезные дополнения для подтипов. Наиболее распространенные из них следующие:
- Any — любой тип;
- Union — любой из указанных типов, например Union[
str
,int
].
В Python, начиная с версии 3.10, можно написать type1 | type2, а не Union[type1, type2].
Примеры определений Pydantic для словарей (dict
) в Python включают следующее:
from typing import Any
responses: dict[str, Any] = {"Marco": "Polo", "answer": 42}
Или, если быть более точными:
from typing import Union
responses: dict[str, Union[str, int]] = {"Marco": "Polo", "answer": 42}
либо (в Python 3.10 и более поздних версиях):
responses: dict[str, str | int] = {"Marco": "Polo", "answer": 42}
Обратите внимание на то, что в Python строка переменной с подсказкой типа является верной, а простая строка переменной — нет:
$ python
...
>>> thing0
Traceback (most recent call last):
File "", line 1, in
NameError: name thing0 is not defined
>>> thing0: str
Кроме того, некорректное использование типов не отлавливается обычным интерпретатором Python:
$ python
...
>>> thing1: str = "yeti"
>>> thing1 = 47
Но такие ошибки будут обнаружены mypy. Если у вас еще не установлен этот статический анализатор, наберите команду pip
install
mypy
. Сохраните две предыдущие строки в файле stuff.py
, а затем попробуйте выполнить следующие команды:
$ mypy stuff.py
stuff.py:2: error: Incompatible types in assignment
(expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)
В подсказке типа возврата функции вместо двоеточия применяется стрелка:
function(args) -> type:
Вот пример возврата функции при использовании Pydantic:
def get_thing() -> str:
return "yeti"
Можно задействовать любой тип, включая определенные классы или их комбинации. Вы увидите это через несколько страниц.
Группировка данных
Зачастую нам нужно сохранить связанную группу переменных, а не передавать множество отдельных переменных. Как объединить несколько переменных в группу и сохранить подсказки типа?
Давайте оставим в прошлом пример с простым приветствием из предыдущих глав и начнем использовать более богатые данные. Как и в остальных частях этой книги, я буду приводить примеры криптидов (воображаемых существ) и исследователей (тоже воображаемых), которые их ищут. Начальные определения криптидов будут включать в себя только строковые переменные для следующих параметров:
-
name
— ключ;country
— двухсимвольный код страны согласно стандарту ISO (3166–1 alpha 2) или *, что означает «все»;area
(необязательный) — штат США или другое территориальное образование страны;description
— в свободной форме;aka
— обозначает «также известен как…» (also known as…).
Пример 5.3 показывает, что вы можете получить немного больше объяснений, определив имена для целочисленных смещений.
Пример 5.3. Использование кортежей и именованных смещений
>>> NAME = 0
>>> COUNTRY = 1
>>> AREA = 2
>>> DESCRIPTION = 3
>>> AKA = 4
>>> tuple_thing = ("yeti", "CN", "Himalayas",
"Hirsute Himalayan", "Abominable Snowman")
>>> print("Name is", tuple_thing[NAME])
Name is yeti
В примере 5.4 словари выглядят немного лучше, предоставляя доступ по описательным ключам.
Пример 5.4. Использование словаря
>>> dict_thing = {"name": "yeti",
... "country": "CN",
... "area": "Himalayas",
... "description": "Hirsute Himalayan",
... "aka": "Abominable Snowman"}
>>> print("Name is", dict_thing["name"])
Name is yeti
Множества содержат только уникальные значения, поэтому они не очень полезны для кластеризации различных переменных.
В примере 5.5 именованный кортеж — это кортеж, предоставляющий вам доступ по целочисленному смещению или имени.
Пример 5.5. Использование именованного кортежа
>>> from collections import namedtuple
>>> CreatureNamedTuple = namedtuple("CreatureNamedTuple",
... "name, country, area, description, aka")
>>> namedtuple_thing = CreatureNamedTuple("yeti",
... "CN",
... "Himalaya",
... "Hirsute HImalayan",
... "Abominable Snowman")
>>> print("Name is", namedtuple_thing[0])
Name is yeti
>>> print("Name is", namedtuple_thing.name)
Name is yeti
Нельзя написать namedtuple_thing[«name»]. Это будет tuple
, а не dict
, поэтому индекс должен быть целым числом.
В примере 5.6 определяется новый класс Python под названием class
и добавляются все атрибуты с помощью self
. Но для их определения вам придется
набрать много текста.
Пример 5.6. Использование стандартного класса
>>> class CreatureClass():
... def __init__(self,
... name: str,
... country: str,
... area: str,
... description: str,
... aka: str):
... self.name = name
... self.country = country
... self.area = area
... self.description = description
... self.aka = aka
...
>>> class_thing = CreatureClass(
... "yeti",
... "CN",
... "Himalayas"
... "Hirsute Himalayan",
... "Abominable Snowman")
>>> print("Name is", class_thing.name)
Name is yeti
Вы можете подумать: что в этом плохого? В обычном классе можно добавить больше данных (атрибутов), но особенно много поведения (методов). В один безумный день вы можете решить добавить метод для поиска любимых песен исследователя. (Это нельзя применить к существам1.) Но в данном случае речь идет о том, чтобы просто без помех перемещать сборки данных между уровнями и проверять их на входе и выходе. Кроме того, методы — это квадратные детали, которые с трудом помещаются в круглые отверстия базы данных.
Есть ли в Python что-то похожее на то, что в других компьютерных языках называется записью (record) или структурой (struct) (группа имен и значений)? Недавно в Python появился класс для хранения данных (dataclass). В примере 5.7 показано, как все эти self-выражения исчезают при использовании классов данных.
Пример 5.7. Применение класса данных dataclass
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class CreatureDataClass():
... name: str
... country: str
... area: str
... description: str
... aka: str
...
>>> dataclass_thing = CreatureDataClass(
... "yeti",
... "CN",
... "Himalayas"
... "Hirsute Himalayan",
... "Abominable Snowman")
>>> print("Name is", dataclass_thing.name)
Name is yeti
Это очень хорошо для части описания, связанной с сохранением переменных вместе. Но нам требуется больше, так что давайте попросим у Дедушки Мороза вот что:
- объединение возможных альтернативных типов;
- отсутствующие/дополнительные значения;
- значения по умолчанию;
- проверку достоверности данных;
- сериализацию в форматы, такие как JSON, и из них.
Альтернативы
Очень заманчиво использовать встроенные структуры данных Python, особенно словари. Но вы неизбежно обнаружите, что словари слишком свободны. А за свободу приходится платить. Вам нужно будет проверить абсолютно все.
- Ключ необязателен?
- Если ключ отсутствует, есть ли значение по умолчанию?
- Существует ли ключ?
- Если да, то относится ли значение ключа к правильному типу?
- Если да, то находится ли значение в нужном диапазоне или соответствует ли
оно шаблону?
По крайней мере три решения отвечают хотя бы некоторым из этих требований:
- Dataclasses (https://oreil.ly/mxANA) — часть стандартного языка Python;
- attrs (https://www.attrs.org) — сторонний пакет, но содержит супернабор классов
данных; - Pydantic (https://docs.pydantic.dev) — тоже сторонний продукт, но интегрированный в FastAPI, поэтому его легко выбрать, если вы уже используете FastAPI.
И если вы читаете эту книгу, то вполне вероятно, что это именно так.
Удобное сравнение этих трех вариантов можно посмотреть на YouTube (https://oreil.ly/pkQD3). Одним из выводов является то, что Pydantic выделяется при проверке, а его интеграция с FastAPI позволяет выявить множество потенциальных ошибок в данных. Другое дело, что Pydantic полагается на наследование (от класса BaseModel
), а два других используют декораторы Python для определения своих объектов. Это скорее вопрос стиля.
В другом сравнении (https://oreil.ly/gU28a) Pydantic превзошел более старые пакеты проверки, такие как marshmallow (https://marshmallow.readthedocs.io) и библиотека с интригующим названием Voluptuous1 (https://github.com/alecthomas/voluptuous). Еще один большой плюс Pydantic в том, что он использует стандартный синтаксис подсказок типов Python — более старые библиотеки не применяли подсказки типов и создавали собственные.
В книге я остановился на Pydantic, но вы можете найти применение любой из альтернатив, если не используете FastAPI.
Pydantic предоставляет возможность задать любую комбинацию следующих проверок:
- обязательные и необязательные;
- значение по умолчанию, если не указано, но требуется;
- ожидаемый тип или типы данных;
- ограничения диапазона значений;
- другие проверки на основе функций, если необходимо;
- сериализацию и десериализацию.
Простой пример
Вы уже видели, как передать простую строку в конечную точку веб-приложения через URL, параметр запроса или тело HTTP-запроса. Проблема в том, что обычно вы запрашиваете и получаете группы данных разных типов. Именно здесь в FastAPI впервые появляются модели Pydantic. В начальном примере будут использоваться три файла:
model.py
— определяет модель Pydantic;-
data.py
— источник фиктивных данных, определяющих экземпляр модели; web.py
— определяет конечную точку веб-приложения FastAPI, возвращающую фиктивные данные.
Для простоты в этой главе сохраним все файлы в одном каталоге. В последующих главах, посвященных более крупным веб-сайтам, мы разделим их на соответствующие уровни. Сначала определим модель существа в примере 5.8.
Пример 5.8. Определение модели существа: model.py
from pydantic import BaseModel
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
thing = Creature(
name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman")
)
print("Name is", thing.name)
Класс Creature
наследуется от класса BaseModel из Pydantic. Часть выражения: str
после слов name
, country
, area
, description
и aka
представляет собой под-
сказку типа — каждое из значений относится к строковому типу данных Python.
В этом примере все поля обязательны для заполнения. В Pydantic, если слово Optional отсутствует в описании типа, поле должно содержать значение.
В примере 5.9 аргументы передаются в любом порядке, если вы указываете их
имена.
Пример 5.9. Создание существа
>>> thing = Creature(
... name="yeti",
... country="CN",
... area="Himalayas"
... description="Hirsute Himalayan",
... aka="Abominable Snowman")
>>> print("Name is", thing.name)
Name is yeti
Пока что в примере 5.10 определен небольшой источник данных. В последующих главах этим будут заниматься базы данных. Подсказка типа list[Creature] говорит Python, что это список только объектов Creature.
Пример 5.10. Определение фиктивных данных в файле data.py
from model import Creature
_creatures: list[Creature] = [
Creature(name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman"
),
Creature(name="sasquatch",
country="US",
area="*",
description="Yeti's Cousin Eddie",
aka="Bigfoot")
]
def get_creatures() -> list[Creature]:
return _creatures
(Мы использовали символ »*» для аргумента area объекта Bigfoot
, потому что он может жить почти везде.)
Этот код импортирует написанный нами ранее файл model.py. Он немного скрывает данные, вызывая свой список объектов Creature_creatures
и предоставляя функцию get_creatures
() для их возврата.
В примере 5.11 приведен файл web.py
, определяющий конечную точку веб-приложения FastAPI.
Пример 5.11. Определение конечной точки веб-приложения FastAPI: web.py
from model import Creature
from fastapi import FastAPI
app = FastAPI()
@app.get("/creature")
def get_all() -> list[Creature]:
from data import get_creatures
return get_creatures()
Теперь запустите этот сервер с одной конечной точкой в примере 5.12.
Пример 5.12. Запуск Uvicorn
$ uvicorn creature:app
INFO: Started server process [24782]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
В другом окне примера 5.13 осуществляется доступ к веб-приложению с помощью веб-клиента HTTPie (попробуйте использовать свой браузер или модуль Requests по желанию).
Пример 5.13. Проверка с помощью HTTPie
$ http http://localhost:8000/creature
HTTP/1.1 200 OK
content-length: 183
content-type: application/json
date: Mon, 12 Sep 2022 02:21:15 GMT
server: uvicorn
[
{
"aka": "Abominable Snowman",
"area": "Himalayas",
"country": "CN",
"name": "yeti",
"description": "Hirsute Himalayan"
},
{
"aka": "Bigfoot",
"country": "US",
"area": "*",
"name": "sasquatch",
"description": "Yeti's Cousin Eddie"
}
FastAPI и Starlette автоматически преобразуют исходный список объектов модели Creature в строку JSON. Это формат вывода по умолчанию в FastAPI, поэтому нам не нужно его указывать.
Кроме того, в окне, в котором вы первоначально запустили веб-сервер Uvicorn, должна быть выведена строка журнала:
INFO: 127.0.0.1:52375 - "GET /creature HTTP/1.1" 200 OK
Проверка типов
В предыдущем разделе было показано, как сделать следующее:
- применить подсказки типов к переменным и функциям;
- определить и использовать модель Pydantic;
- возвратить список моделей из источника данных;
- возвратить список моделей веб-клиенту, автоматически преобразовав его
в JSON.
А теперь действительно применим этот план для проверки данных. Попробуйте присвоить значение неправильного типа одному или нескольким полям объекта Creature. Для этого воспользуйтесь автономным тестом (Pydantic не применяется ни к какому веб-коду, он относится к данным).
В примере 5.14 показано содержимое файла test1.py.
Пример 5.14. Проверка модели Creature
from model import Creature
dragon = Creature(
name="dragon",
description=["incorrect", "string", "list"],
country="*" ,
area="*",
aka="firedrake")
Теперь попробуйте выполнить тест из примера 5.15. Он показывает, что мы присвоили полю description
список строк, а ему нужна обычная строка.
Пример 5.15. Продолжение теста
$ python test1.py
Traceback (most recent call last):
File ".../test1.py", line 3, in
dragon = Creature(
File "pydantic/main.py", line 342, in
pydantic.main.BaseModel.init
pydantic.error_wrappers.ValidationError:
1 validation error for Creature description
str type expected (type=type_error.str)
Проверка значений
Даже если тип значения соответствует его спецификации в классе Creature
,
могут потребоваться дополнительные проверки. Некоторые ограничения могут быть наложены на само значение.
- Целочисленное значение (
conint
) или число с плавающей точкой:gt
— больше чем;lt
— меньше чем;ge
— больше или равно;le
— меньше или равно;multiple_of
— целое число, кратное значению. - Строковое (
constr
) значение:min_length
— минимальная длина в символах (не в байтах);max_length
— максимальная длина в символах;to_upper
— преобразование в прописные буквы;to_lower
— преобразование в строчные буквы;regex
— сопоставление с регулярным выражением Python. - Кортеж, список или множество:
min_items
— минимальное количество элементов;max_items
— максимальное количество элементов.
Они указываются в типовых частях модели.
Пример 5.16 позволяет убедиться, что поле name всегда будет содержать не менее двух символов. В противном случае » (пустая строка) будет считаться допустимой.
Пример 5.16. Просмотр ошибки проверки
>>> from pydantic import BaseModel, constr
>>>
>>> class Creature(BaseModel):
... name: constr(min_length=2)
... country: str
... area: str
... description: str
... aka: str
...
>>> bad_creature = Creature(name="!",
... description="it's a raccoon",
... area="your attic")
Traceback (most recent call last):
File "", line 1, in
File "pydantic/main.py", line 342,
in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError:
1 validation error for Creature name
ensure this value has at least 2 characters
(type=value_error.any_str.min_length; limit_value=2)
Ключевое слово constr означает ограниченную строку (constrained string).
В примере 5.17 используется альтернативный вариант — спецификация Field
из библиотеки Pydantic.
Пример 5.17. Еще один сбой проверки, применена функция Field
>>> from pydantic import BaseModel, Field
>>>
>>> class Creature(BaseModel):
... name: str = Field(..., min_length=2)
... country: str
... area: str
... description: str
... aka: str
...
>>> bad_creature = Creature(name="!",
... area="your attic",
... description="it's a raccoon")
Traceback (most recent call last):
File "", line 1, in
File "pydantic/main.py", line 342,
in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError:
1 validation error for Creature name
ensure this value has at least 2 characters
(type=value_error.any_str.min_length; limit_value=2)
Аргумент… функции Field
() означает, что значение обязательное и значения по умолчанию не предусмотрено.
Это минимальное введение в Pydantic. Главное, что можно сделать, — автоматизировать проверку данных. Вы увидите, насколько это полезно, при получении данных с веб-уровня или уровня данных.
Заключение
Модели предоставляют лучший способ определить данные, передаваемые в вашем веб-приложении. Библиотека Pydantic использует подсказки типов Python для определения моделей, передаваемых в приложении данных. Далее — определение зависимостей для выделения конкретных деталей из общего кода.
Билл Любанович занимается разработкой ПО уже более 40 лет, специализируясь на Linux, Web и Python. Билл выступил соавтором книги «Системное администрирование в Linux» и написал «Простой Python».
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — FastAPI