Python >> Tutoriel Python >  >> Python

FastAPI vs Flask :comparaison des avantages et des inconvénients des meilleurs microframeworks pour la création d'une API REST en Python

La création d'applications Web telles que les API REST est le pain quotidien des développeurs backend. Par conséquent, travailler avec un framework Web devrait être rapide et facile.

Les microframeworks sont un bon début pour les petits projets, les MVP ou même les grands systèmes qui ont besoin d'une API REST, y compris Flask et FastAPI.

J'ai écrit une application pour créer, mettre à jour, télécharger et supprimer des actualités dans ces deux frameworks. En conséquence, voici ma comparaison de FastAPI et Flask.

Qu'est-ce que Flask ? Pourquoi l'utiliser ?

Flask est l'une des bibliothèques les plus populaires pour créer des applications Web en Python. Les personnes qui commencent leur aventure avec la programmation trouveront facilement de nombreux didacticiels Flask et des solutions aux problèmes courants.

Il est léger (un "microframework") et très bien documenté, avec de nombreuses extensions et une grande communauté.

Qu'est-ce que FastAPI ? Pourquoi l'utiliser ?

FastAPI se classe parmi les frameworks Web Python les plus performants pour la création d'API et est de plus en plus utilisé de jour en jour.

Son accent mis sur la vitesse, non seulement en termes de nombre de requêtes traitées par seconde, mais aussi de vitesse de développement et de sa validation de données intégrée, en fait un candidat idéal pour le côté backend de notre application Web.

Validation des données

Voici où nous pouvons trouver la première différence significative entre les deux bibliothèques.

En installant Flask, nous n'obtenons aucun outil de validation des données. Cependant, nous pouvons contourner cela en utilisant des extensions proposées par la communauté, telles que Flask-Marshmallow ou Flask-Inputs.

L'inconvénient de cette solution est que nous devons nous appuyer sur des bibliothèques développées séparément de notre framework principal, ce qui signifie que nous ne pouvons pas être sûrs à 100 % qu'elles seront compatibles.

FastAPI, d'autre part, nous donne la bibliothèque Pydantic à utiliser, ce qui rend la validation des données beaucoup plus simple et plus rapide que de les taper à la main. Il est étroitement lié à FastAPI lui-même, nous pouvons donc être sûrs que Pydantic sera compatible avec notre framework à tout moment.

Alors, quelles sont les validations dans les bibliothèques individuelles basées sur notre simple API ?

Nous créons des classes nommées `NewsSchema` / `CreatorSchema` qui seront les classes de base pour valider nos actualités et auteurs.

   # Flask
@dataclass()
class NewsSchema(BaseSchema):
title: str = ""
content: str = ""
creator: CreatorSchema = CreatorSchema()

@dataclass
class CreatorSchema(BaseSchema):
first_name: str = ""
last_name: str = ""
   # FastAPI
class NewsSchema(BaseModel):
title: str = ""
content: str = ""
creator: CreatorSchema

class CreatorSchema(BaseModel):
first_name: str = ""
last_name: str = ""

Nous pouvons remarquer que `NewsSchema` / `CreatorSchema` de FastAPI utilise `BaseModel` comme classe parent. Ceci est nécessaire car `BaseModel` provient de la bibliothèque Pydantic et possède les fonctions nécessaires à la validation des données.

Dans Flask, cependant, nous héritons de la classe `BaseSchema`, qui est une classe de données normale et contient plusieurs méthodes que les classes héritantes utiliseront ou remplaceront.

Dans notre cas, nous vérifierons uniquement si le texte que nous saisissons respecte la limite de caractères.

La validation proprement dite aura lieu dans les classes `NewsSchemaInput` / `CreatorSchemaInput` :

   # Flask
@dataclass()
class NewsSchemaInput(NewsSchema):
_errors: dict = field(init=False, default_factory=dict)

def _validate_title(self) -> None:
if MIN_TITLE_LEN > len(self.title) < MAX_TITLE_LEN:
self._errors[
"title"
] = f"Title should be {MIN_TITLE_LEN}-{MAX_TITLE_LEN} characters long"

def _validate_content(self) -> None:
if len(self.content) < MIN_CONTENT_LEN:
self._errors[
"content"
] = f"Content should be minimum {MIN_CONTENT_LEN} characters long"

def __post_init__(self) -> None:
self._validate_content()
self._validate_title()
try:
if not isinstance(self.creator, CreatorSchemaInput):
self.creator = CreatorSchemaInput(**self.creator)
except ValidationError as err:
self._errors["creator"] = err.errors
if self._errors:
raise ValidationError(
f"Validation failed on {type(self).__name__}", self._errors
)
   # Flask
@dataclass
class CreatorSchemaInput(CreatorSchema):
_errors: dict = field(init=False, default_factory=dict)

def _validate_first_name(self) -> None:
if FIRST_NAME_MIN_LEN > len(self.first_name) < FIRST_NAME_MAX_LEN:
self._errors[
"first_name"
] = f"First name should be {FIRST_NAME_MIN_LEN}-{FIRST_NAME_MAX_LEN} characters long"

def _validate_last_name(self) -> None:
if LAST_NAME_MIN_LEN > len(self.last_name) < LAST_NAME_MAX_LEN:
self._errors[
"last_name"
] = f"Last name should be {LAST_NAME_MIN_LEN}-{LAST_NAME_MAX_LEN} characters long"

def __post_init__(self) -> None:
self._validate_first_name()
self._validate_last_name()
if self._errors:
raise ValidationError(
f"Validation failed on {type(self).__name__}", self._errors
)

Lorsque nous créons notre objet `NewsSchemaInput` / `CreatorSchemaInput`, la méthode `__post_init__` sera exécutée, où nous exécutons la validation des données (vérification de la longueur du texte). Si c'est incorrect, nous ajoutons des erreurs à la variable `_errors`, et finalement levons une exception `Validation Error`.

Dans le cas de structures imbriquées (`CreatorSchemaInput`), nous devons créer ces objets manuellement. Nous le faisons après la validation de `NewsSchemaInput` dans la méthode `__post_init__`.

La vérification des données elle-même n'est pas un gros problème - seul l'ajout de nouveaux champs sera fastidieux, car nous devons ajouter une méthode `_validate` distincte à chaque fois. Dans le cas d'une structure imbriquée, nous devons créer une instance de cet objet et intercepter une exception.

Nous pouvons voir que les classes qui valident les données entrantes deviennent assez étendues, et cela ne concerne que quelques clés. Nous devons également ajouter notre propre implémentation de la gestion des erreurs, afin de pouvoir ajouter des informations d'erreur imbriquées dans les réponses de l'API.

Dans FastAPI, c'est beaucoup plus simple et plus agréable :

   # FastAPI
class NewsSchemaInput(NewsSchema):
title: str = Field(
title="Title of the News",
max_length=MAX_TITLE_LEN,
min_length=MIN_TITLE_LEN,
example="Clickbait title",
)
content: str = Field(
title="Content of the News", min_length=50, example="Lorem ipsum..."
)
creator: CreatorSchemaInput
   # FastAPI
class CreatorSchemaInput(CreatorSchema):
first_name: str = Field(
title="First name of the creator",
min_length=FIRST_NAME_MIN_LEN,
max_length=FIRST_NAME_MAX_LEN,
example="John",
)
last_name: str = Field(
title="Last name of the creator",
min_length=LAST_NAME_MIN_LEN,
max_length=LAST_NAME_MAX_LEN,
example="Doe",
)

En important `Field` depuis `Pydantic`, nous avons accès à des règles simples qui doivent être suivies pour que l'entrée de l'utilisateur soit valide. Les types de données sont également validés sur la base des types de variables, donc si notre variable `first_name` a le type `str`, nous devons passer du texte dans l'entrée (et agir de la même manière pour tous les types de données intégrés).

Sans aucun code supplémentaire, Pydantic fait un excellent travail de vérification des structures imbriquées (`CreatorSchemaInput` dans ce cas).

Nous pouvons trouver tout cela en quelques lignes de code seulement !

En plus de `max_length` et `min_length`, nous pouvons également voir deux paramètres supplémentaires :`title` et `example`. Ils sont facultatifs, mais seront visibles dans la documentation automatique générée par FastAPI pour nous.

Sérialisation des données sortantes

Maintenant que nous savons comment valider les données, nous devons réfléchir à la manière dont nous voulons les restituer.

Le message aura non seulement le contenu, le titre et l'auteur, mais aussi son numéro unique (id) et la date à laquelle il a été créé et mis à jour. Nous devons créer une nouvelle classe qui sérialisera le modèle de domaine `News` et ce sera `NewsSchemaOutput`.

   # Flask
@dataclass
class NewsSchemaOutput(NewsSchema):
id: int = 0
created_at: datetime = datetime.now()
updated_at: datetime = datetime.now()

def as_dict(self) -> dict:
schema_as_dict = super().as_dict()
schema_as_dict["created_at"] = int(self.created_at.timestamp())
schema_as_dict["updated_at"] = int(self.updated_at.timestamp())
return schema_as_dict
   # FastAPI
class NewsSchemaOutput(NewsSchema):
id: int = Field(example="26")
created_at: datetime = Field(example="1614198897")
updated_at: datetime = Field(example="1614198897")

class Config:
json_encoders = {datetime: lambda dt: int(dt.timestamp())}

La classe `NewsSchemaOutput` est pratiquement la même dans les deux cas, la seule différence étant la classe parente et la méthode de sérialisation vers le dictionnaire (ainsi que la modification de l'objet `datetime` en horodatage).

Dans FastAPI, lors de l'utilisation de Pydantic, nous avons la possibilité d'ajouter une classe `Config`, dans laquelle nous avons placé la variable `json_encoders`. Cela aide à sérialiser les données de la manière dont nous avons besoin. Dans ce cas, nous souhaitons transmettre l'objet date en tant qu'horodatage. Dans Flask, cependant, nous avons dû changer les données du dictionnaire déjà créé en celles que nous voulons renvoyer.

Créer des vues et définir des données

La configuration des messages dans les deux bibliothèques est très similaire et utilise un simple décorateur sur la fonction que nous voulons utiliser. Cependant, les façons de définir la validation et la sérialisation des données diffèrent.

   # Flask
@news_router.route("/news", methods=["POST"])
def add_news():
db_repo = get_database_repo()
news_schema = NewsSchemaInput(**request.get_json())
news_dto = NewsDTO.from_news_schema(news_schema=news_schema)
saved_news = db_repo.save_news(news_dto=news_dto)
output_schema = NewsSchemaOutput.from_entity(news=saved_news).as_dict()
return output_schema, HTTPStatus.CREATED
   # FastAPI
@news_router.post(
"/news",
response_model=NewsSchemaOutput,
summary="Create the news",
status_code=status.HTTP_201_CREATED,
)
async def add_news(
news_input: NewsSchemaInput,
db_repo: DatabaseRepository = Depends(get_database_repo),
):
"""
Create the news with following information:

- **title**: Title of news
- **content**: News content
- **creator**: Creator of content
"""
news_dto = NewsDTO.from_news_schema(news_schema=news_input)
db_news = await db_repo.save_news(news_dto=news_dto)
return db_news.as_dict()

Au tout début, nous avons un décorateur qui spécifie le chemin et la méthode HTTP qui seront manipulés. Flask le définit en utilisant le paramètre `methods`, où nous devons transmettre la liste des méthodes prises en charge, tandis que FastAPI utilise l'attribut `post` sur `news_router`.

Le décorateur utilisé par FastAPI sert non seulement à déterminer le chemin et les méthodes HTTP, mais également à sérialiser les données (`response_model`), décrire la vue dans la documentation automatique (`summary`), définir le statut de la réponse (`status_code `), et bien plus encore - toutes ses fonctions n'ont pas été incluses dans cet exemple.

On peut dire que FastAPI définit non seulement le chemin d'accès et la méthode, mais décrit également l'ensemble de la vue en profondeur. Mais que se passe-t-il vraiment dans cette vue ? Commençons par Flask !

La première chose que nous faisons est d'obtenir le référentiel de base de données pour notre fonction avec :db_repo =get_database_repo ()

Dans l'étape suivante, nous validons les données soumises par l'utilisateur, qui se trouvent dans l'objet `request` :

   db_repo = get_database_repo()
   news_schema = NewsSchemaInput(**request.get_json())

Cette ligne déclenchera une exception `ValidationError` si l'entrée n'est pas valide.

L'exception sera capturée dans le `errorhandler` que nous avons créé et Flask renverra une réponse avec toutes les erreurs qui se trouvent dans la variable `_errors` sur `NewsSchemaInput`.

Mais attendez juste une seconde ! Nous n'avons pas encore discuté du "gestionnaire d'erreurs" que nous avons soi-disant créé.

Dans Flask et FastAPI, nous pouvons ajouter notre propre gestion des exceptions, qui sera lancée dans l'implémentation des vues. Ils ressemblent à ceci :

   # Flask
@app.errorhandler(ValidationError)
def handle_validation_error(exc: ValidationError) -> Tuple[dict, int]:
status_code = HTTPStatus.UNPROCESSABLE_ENTITY
return {"detail": exc.errors}, status_code
   # FastAPI
@app.exception_handler(ValidationError)
async def handle_validation_error(request: Request, exc: ValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": exc.errors()},
)

Si la validation a réussi, créez un objet `NewsDTO` qui transmettra les informations nécessaires au référentiel de la base de données. Le référentiel fera sa magie (enregistrera un message dans la base de données) et nous renverra l'objet de domaine `News`, que nous sérialiserons ensuite avec la classe `NewsSchemaOutput` :

   news_dto = NewsDTO.from_news_schema(news_schema=news_schema)
saved_news = db_repo.save_news(news_dto=news_dto)
output_schema = NewsSchemaOutput.from_entity(news=saved_news).as_dict()

À la toute fin, nous renvoyons `NewsSchemaOutput` comme dictionnaire et le statut de la réponse :

   return output_schema, HTTPStatus.CREATED

Maintenant, regardons FastAPI. Cette fois, nous obtenons deux paramètres dans la vue :`news_input` et` db_repo`.

Dans le premier, la validation des données d'entrée a lieu avant l'exécution de notre méthode de vue, grâce au paramètre `news_input`.

Vous vous demandez peut-être :comment FastAPI sait-il quelle classe utiliser ? C'est grâce à la frappe. Le paramètre `news_input` a le type ` NewsSchemaInput`, donc ce que fait FastAPI est de transmettre toutes les données à cette classe que nous avons envoyées en utilisant la méthode POST. Nous n'avons pas besoin de créer une instance de l'objet `NewsSchemaInput` car nous obtiendrons des données validées dans le paramètre `news_input`.

En ce qui concerne `db_repo`, cela fonctionne de la même manière que Flask, sauf qu'ici nous utilisons l'injection de dépendances. Le mot-clé `Depends` vous permet de substituer des classes ou des fonctions pendant que notre application est en cours d'exécution. Nous parlerons de "l'injection de dépendances" un peu plus tard.

   async def add_news(
news_input: NewsSchemaInput,
db_repo: DatabaseRepository = Depends(get_database_repo),
):

Lorsque notre méthode est appelée, nous enregistrons le message dans la base de données.

   db_news = await db_repo.save_news(news_dto=news_dto)

Dans Flask, nous avons dû créer une instance de la classe `NewsSchemaOutput` pour renvoyer les données correctes. Idem pour le statut de la réponse :il est également renvoyé à l'aide du mot-clé "return".

FastAPI vous permet de spécifier une classe pour sérialiser les données à l'aide du paramètre `response_model` dans le décorateur. Tout ce que nous avons à faire est de fournir la structure correcte que `Pydatnic` comprendra. Le statut de la réponse peut également être défini au même endroit que `response_model`, mais en utilisant le paramètre` status_code`.

Récupération des messages, des variables dans l'adresse et des paramètres GET

Tout comme lorsque nous créons un article, nous définissons la vue avec un simple décorateur. Cette fois, cependant, nous utilisons la méthode GET.

   # Flask
@news_router.route("/news/<int:news_id>", methods=["GET"])
def get_news(news_id: int):
db_repo = get_database_repo()
news_from_db = db_repo.get_news(news_id=news_id)
output_schema = NewsSchemaOutput.from_entity(news=news_from_db).as_dict()
return output_schema
   # FastAPI
@router.get(
"/news/{news_id}",
response_model=NewsSchemaOutput,
summary="Get the news by ID",
responses=NOT_FOUND_FOR_ID,
)
async def get_news(
news_id: int, db_repo: DatabaseRepository = Depends(get_database_repo)
):
"""
Get the news with passed ID
"""
db_news = await db_repo.get_news(news_id=news_id)
return db_news.as_dict()

Pour télécharger le message qui nous intéresse, nous devons transmettre son identifiant à notre vue. Nous faisons cela avec une adresse à laquelle nous ajoutons le paramètre `news_id`. Dans Flask, nous devons spécifier son type en détail à l'aide de crochets angulaires et du nom, c'est-à-dire ``. Nous sommes obligés d'utiliser uniquement des types de base que Flask comprend, tels que int, uuid, str ou float, etc.

FastAPI utilise une convention similaire à celle utilisée par f-string, où le nom de notre variable est défini par des accolades et son type est défini dans les paramètres de la fonction d'affichage.

C'est une solution plus flexible, car nous pouvons essayer de passer des structures compliquées dans l'adresse. Vous avez peut-être également remarqué un nouveau paramètre qui est apparu dans le décorateur de vue. Ce paramètre s'appelle `responses` - nous y reviendrons lorsque nous discuterons de la documentation automatique.

Filtrer les messages avec les paramètres GET

Lorsque nous voulons une solution flexible, au lieu de créer une vue qui nécessite des variables définies dans l'adresse, nous utilisons des paramètres GET. Dans ce cas, nous devons renvoyer des messages qui répondent aux critères qui nous sont transmis par les "paramètres de requête". Nous avons deux paramètres :`id` et `created_at`.

   # Flask
@news_router.route("/news", methods=["GET"])
def get_news_by_filter():
db_repo = get_database_repo()
ids = request.args.getlist("id", type=int)
created_at = request.args.getlist("created_at", type=int)
news_from_db = db_repo.get_news_by_filter(id=ids, created_at=created_at)
return jsonify(
[NewsSchemaOutput.from_entity(news=news).as_dict() for news in news_from_db]
)
   # FastAPI
@router.get(
"/news",
response_model=List[NewsSchemaOutput],
summary="Get the news by filter",
responses=NOT_FOUND_FOR_ID,
)
async def get_news_by_filter(
id: Set[int] = Query(set()),
created_at: Set[datetime] = Query(set()),
db_repo: DatabaseRepository = Depends(get_database_repo),
):
"""
Get the news with passed filters.

- **id**: List of id to search for
- **created_at**: List of date of creation timestamps
"""
db_news = await db_repo.get_news_by_filter(id=id, created_at=created_at)
return [news.as_dict() for news in db_news]

Flask fournit l'objet de requête à partir duquel nous pouvons extraire des données sur la requête à notre méthode de vue. Flask propose un objet "request" à partir duquel nous pouvons récupérer toutes les données de requête à notre vue.

Cette fois, nous nous intéressons aux paramètres `id` et `created_at`. Nous savons également que nous pouvons nous attendre à une liste de ces paramètres. Pour cela, nous utilisons la méthode `getlist` du dictionnaire spécial `args`.

   ids = request.args.getlist("id", type=int)
created_at = request.args.getlist("created_at", type=int)

Ensuite, nous envoyons les données extraites au référentiel de la base de données pour obtenir une liste de modèles de domaine `News`, que nous transformons en une liste de dictionnaires de la classe `NewsSchemaOutput`.

   news_from_db = db_repo.get_news_by_filter(id=ids, created_at=created_at)
[NewsSchemaOutput.from_entity(news=news).as_dict() for news in news_from_db]

Nous devons également nous rappeler que nous ne pouvons pas renvoyer la liste depuis la vue. Il est nécessaire d'exécuter la fonction `jsonify` pour que notre point de terminaison renvoie l'objet `Response` avec la sérialisation correcte de la liste.

   return jsonify(
[NewsSchemaOutput.from_entity(news=news).as_dict() for news in news_from_db]
)

Avec FastAPI, l'ensemble du processus ressemble assez à Flask - la différence est que nous obtenons les variables d'adresse dans les paramètres de la fonction, ce qui est beaucoup plus lisible que d'exécuter `request.args.getlist` avec chaque variable dont nous avons besoin. Pour que FastAPI sache que les paramètres de la fonction sont des variables d'adresse, nous devons leur ajouter la valeur par défaut `Query`, qui est prédéfinie.

Comment FastAPI sait-il que nous voulons un type de données spécifique si nous ne l'avons pas spécifié entre accolades ? La saisie le montre.

Tout ce que nous avons à faire est d'ajouter un type à nos paramètres, par ex. `set [int]`, et nous serons sûrs que la variable contiendra un ensemble avec des entiers uniquement.

Une fois les variables d'adresse validées, nous extrayons les modèles de domaine "Actualités" du référentiel de la base de données à l'aide des critères envoyés. Ensuite, nous renvoyons la liste des dictionnaires de modèles de message et le `response_model` dans le décorateur s'occupera de la sérialisation correcte des données.

   db_news = await db_repo.get_news_by_filter(id=id, created_at=created_at)
return [news.as_dict() for news in db_news]

Injection de dépendance

L'injection de dépendances est un modèle de conception et d'architecture logicielle basé sur la suppression des dépendances directes entre les composants.

Cela semble assez compliqué, n'est-ce pas ? Eh bien, FastAPI a pu implémenter ce modèle de manière très simple.

Nous avons peut-être remarqué que dans chaque vue, il y a quelque chose comme ceci dans les paramètres de la fonction :

   db_repo: DatabaseRepository = Depends(get_database_repo)

C'est ce que nous appelons une injection de dépendance. Dans ce cas, nous injectons le référentiel de la base de données. Le mot clé `Depends` est capable d'injecter tout ce qui peut être nommé (par exemple des classes ou des fonctions). C'est une bonne méthode, car elle vous permet de vous en tenir à la règle DRY (Don't Repeat Yourself), car vous n'avez pas à créer une nouvelle variable pour le référentiel de base de données à chaque fois, comme cela se fait dans Flask :

   db_repo = get_database_repo()

Un autre avantage de `Depends` est qu'il peut facilement remplacer les implémentations dans les tests.

Dans Flask, pour remplacer la valeur de retour de `get_database_repo`, nous devrions simuler cette fonction à chaque fois que nous exécutons des tests.

   @mock.patch("path.to.dependency.get_database_repo)
def test_some_view(db_repo_inject_mock):
db_repo_inject_mock.return_value = OUR OWN DB REPO IMPLEMENTATION

Grâce à l'injection de dépendances dans FastAPI. nous pouvons utiliser…

   app.dependency_overrides[db_repo] = OUR OWN CALLABLE IMPLEMENTATION

…pour remplacer l'implémentation lors de l'exécution des tests.

`Depends` peut également être utilisé pour ne pas répéter les mêmes paramètres de fonction n fois. Pour en savoir plus, consultez la documentation.

Asynchronicité

Malheureusement, Flask ne prend pas en charge l'asynchronicité et l'interface ASGI, ce qui signifie que certaines requêtes de longue durée peuvent bloquer notre application. Cela est lié au nombre réduit d'utilisateurs que nous pouvons gérer avec notre API REST.

Comme vous l'avez peut-être remarqué, les fonctions d'affichage dans FastAPI commencent par `async` et chaque méthode appelant le référentiel de la base de données est précédée du mot `await`.

FastAPI est entièrement asynchrone, ce qui ne veut pas dire qu'il est nécessaire, puisque nous pouvons également implémenter des fonctions synchrones ordinaires, et utilise l'interface ASGI. Grâce à cela, nous pouvons utiliser des requêtes non bloquantes vers des bases de données ou des services externes, ce qui signifie que le nombre d'utilisateurs simultanés utilisant notre application sera beaucoup plus important que dans le cas de Flask.

Dans sa documentation, FastAPI a un exemple très bien écrit d'utilisation de `async` et `wait`. Je recommande fortement de le lire !

Et si vous exécutiez un benchmark ?

Pour cette tâche, nous utiliserons Locust. C'est un outil de test de charge Python gratuit et open source. Notre test sera basé sur l'ajout de 100 utilisateurs au pool de connexions actives chaque seconde, jusqu'à ce que nous atteignions 2 000 utilisateurs en même temps.

Flasque

Comme nous pouvons le voir, le nombre de requêtes par seconde que nous pouvons traiter est d'environ 633. Ce n'est pas mal, n'est-ce pas ? Cela pourrait être mieux, cependant. Le temps d'attente moyen pour une réponse est d'environ 1 642 ms - pratiquement une seconde et demie pour recevoir des données de l'API, c'est définitivement trop. A cela, on peut ajouter 7% de requêtes infructueuses.

FastAPI

FastAPI a fait beaucoup mieux dans cette tâche. Le nombre de requêtes que nous pouvons traiter est d'environ 1 150 par seconde (presque deux fois plus que dans Flask), et le temps d'attente moyen d'une réponse n'est que de… 14 ms. Toutes les requêtes étaient correctes et nous n'avons détecté aucune erreur.

Documentation automatique

Lors de la création d'une API REST, la documentation est essentielle pour une équipe de développeurs ou d'utilisateurs qui souhaitent utiliser cette interface pour communiquer avec notre application.

Vous pouvez le faire manuellement, par ex. dans le wiki Jira Confluence / Github ou tout autre outil de collecte de données de conception. Cependant, il existe un risque d'erreur humaine, par ex. quand quelqu'un oublie de mettre à jour les adresses des vues ou fait une faute de frappe.

La norme la plus courante pour créer une telle documentation est OpenAPI et JSONSchema.

Flask propose des extensions, telles que Flask-Swagger ou Flasgger, qui fonctionnent à l'aide de la spécification mentionnée ci-dessus. Ils nécessitent une installation supplémentaire et une connaissance du format utilisé par ces normes.

De plus, les spécifications des données transférées doivent être enregistrées manuellement - elles ne seront pas extraites des classes qui valident ou des paramètres que nous téléchargeons.

FastAPI dispose d'une documentation entièrement compatible avec OpenAPI et JSONSchema, qui est créée automatiquement à partir de schémas Pydantic et de paramètres de fonction ou de variables GET. L'interface utilisateur est fournie par SwaggerUI et Redoc.

C'est une fonctionnalité très intéressante, car elle ne nécessite aucun travail de notre part (sauf si nous voulons agrémenter notre documentation de détails). Toutes les règles pour les données requises se trouvent dans les schémas Pydatnic.

La documentation est disponible sur `host / doc` (SwaggerUI) et `host / redoc` (ReDoc) et ressemble à ceci :

SwaggerUI

ReDoc

Dans SwaggerUI, nous avons également accès à tous les schémas que nous avons définis dans notre application :

On peut remarquer que les informations des paramètres `summary` et `title` de `CreatorSchemaInput` sont apparues.

Comment FastAPI sait-il quelles informations transmettre à la documentation ? Regardons un exemple de téléchargement de messages :

   # FastAPI
@router.get(
"/news/{news_id}",
response_model=NewsSchemaOutput,
summary="Get the news by ID",
responses=NOT_FOUND_FOR_ID,
)
async def get_news(
news_id: int, db_repo: DatabaseRepository = Depends(get_database_repo)
):
"""
Get the news with passed ID
"""
db_news = await db_repo.get_news(news_id=news_id)
return db_news.as_dict()

Certains paramètres du décorateur seront pris en compte lors de la création de la documentation :

  • `/ news / {news_id}` — dans la documentation, nous verrons que le paramètre `news_id` est obligatoire et doit être un entier
  • `response_model`—ce schéma de réponse sera automatiquement affiché dans la documentation
  • `responses` :si notre vue renvoie des codes de réponse autres que 200/400/422 ou 500, nous pouvons ajouter un dictionnaire spécial avec les statuts et le schéma de données renvoyé, comme ici :
   NOT_FOUND_FOR_ID: Response_Type = {
404: {
"description": "News with given ID wasn't found",
"content": {
"application/json": {"example": {"detail": "News with id {id} don't exist"}}
},
}
}

En outre, la docstring est prise en compte et sera affichée comme information supplémentaire pour la vue spécifique.

Réflexions finales sur Flask et FastAPI

Merci d'avoir lu ma comparaison de ces deux grandes bibliothèques en utilisant une application CRUD très simple à partir d'une API REST comme exemple.

D'un côté, nous avons le très populaire Flask, qui ne peut être ignoré ; de l'autre, il y a FastAPI, qui gagne le cœur des utilisateurs avec le nombre de fonctionnalités intégrées et l'asynchronicité.

Alors, lequel est le meilleur ? Personnellement, si je devais choisir le framework pour mon prochain projet REST, je pencherais certainement pour FastAPI.

Bien sûr, vous êtes libre de tirer vos propres conclusions et de choisir différemment. Cependant, j'espère que vous essaierez au moins de donner une chance à FastAPI.

Chez STX Next, nous nous spécialisons dans Python et offrons de nombreuses ressources utiles sur le sujet, par exemple, comment il se compare à d'autres langages et à quoi il est le plus utilisé. Enfin, rendez-vous ici pour découvrir l'application que j'ai créée !


Daniel Różycki travaille dans le secteur informatique depuis quatre ans. Il est spécialisé en Python et travaille comme développeur Python chez STX Next depuis un an. Il a commencé sa carrière en travaillant avec les technologies blockchain et s'occupe actuellement de divers projets dans le cadre de son travail de maison de logiciels. Passionné par l'architecture propre et le code propre, il aime aussi programmer pendant son temps libre.