Renvoyer uniquement les valeurs d'une liste d'enregistrements de FastAPI
En ne renvoyant que des valeurs au lieu de dictionnaires, nous réduisons la taille et le temps de transfert des données.
Dans Python, tout est une classe, ce qui signifie que les données du modèle sont similaires à un dictionnaire. Mais les dictionnaires ont des clés. Et lorsque vous renvoyez une liste de plusieurs dictionnaires à partir de FastAPI, la taille des données, clés et valeurs, est généralement deux fois plus importante que la taille des valeurs. Une taille plus importante et un temps plus long signifient que notre application n'est pas très efficace, qu'elle est plus lente que nécessaire. Cela signifie également qu'elle consomme plus d'énergie, ce qui signifie qu'elle n'est pas très durable (ça sonne bien... ugh).
Ci-dessous, je présente et compare deux façons de retourner les données de FastAPI :
- Une liste de dictionnaires
- Une liste de tuples contenant des valeurs de dictionnaire
Pour rendre les choses plus excitantes, la réponse se compose d'une partie "meta" et d'une partie "data". Vous pouvez essayer vous-même, le code est ci-dessous. Comme toujours, je travaille sur Ubuntu 22.04.
La classe ListResponse
J'ai déjà mentionné la classe ListResponse dans un article précédent. Ce que nous voulons retourner, ce sont deux éléments, 'meta' et 'data'.
return {
'meta': ListMetaResponse(
page=1,
per_page=10,
count=count,
total=total,
),
'data': <list-of-dicts or list-of-tuples>,
}
La classe ListResponse crée un seul objet de réponse, en utilisant la classe ListMetaResponse et le modèle de données. Pour l'utiliser :
response_model=ListResponse(Item)
1. Renvoyer une liste de dictionnaires
Veuillez vous référer au document FastAPI "Response Model - Return Type" pour le modèle, voir les liens ci-dessous.
Nous utilisons le modèle et les éléments suivants :
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: list[str] = []
items = [
{
'name': 'Portal Gun',
'price': 42.0
},
{
'name': 'Plumbus',
'price': 32.0
},
]
Le retour des données est très simple ; je ne vous ennuierai pas avec cela ici.
2. Renvoi d'une liste de tuples contenant les valeurs.
Dans ce cas, nous utilisons un tuple pour les valeurs d'un dictionnaire. Nous ne pouvons pas utiliser une liste car il n'y a pas de support de typage Python pour les éléments positionnels dans une liste. Les valeurs du dictionnaire doivent être placées à des positions fixes dans le tuple. Cela signifie que le modèle est, voir ci-dessus :
Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]
L'obtention des valeurs dans le tuples dépend de votre application, de l'origine des données. Ici, je suppose que les "éléments", comme indiqué ci-dessus, sont des dictionnaires "incomplets". Nous pouvons les transformer en une liste de tuples de la manière suivante :
# extract values from items
def get_item_values(item):
d = {
'name': None,
'description': None,
'price': None,
'tax': None,
'tags': None,
} | item
return tuple(d.values())
items_values = list(map(get_item_values, items))
Si les "éléments" proviennent d'une base de données, vous pouvez sélectionner tous les champs de la requête. Le résultat sera une liste de tuples et ce traitement n'est pas nécessaire.
Comparaison des données JSON
1. Liste des dictionnaires
Pour obtenir les données de JSON , exécuter dans un autre terminal :
curl http://127.0.0.1:8888/items
Résultat :
{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]}
Et la partie des données seulement :
[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]
Nombre de bytes des données : 148.
2. Liste des tuples contenant les valeurs
Pour obtenir les données JSON :
curl http://127.0.0.1:8888/items-values
Résultat :
{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]}
Et la partie des données seulement :
[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]
Nombre de bytes des données : 68.
C'est une réduction de 54% !
Le code
Voici le code au cas où vous voudriez essayer. Créez un virtual environment, puis :
pip install fastapi
pip install uvicorn
Lancez l'application :
python main.py
Pour afficher les articles, tapez dans votre navigateur :
http://127.0.0.1:8888/items
Pour afficher les valeurs des éléments, tapez dans votre navigateur :
http://127.0.0.1:8888/items-values
Le code :
# main.py
import datetime
from functools import lru_cache
from typing import Any, List, Optional, Tuple, Union
from fastapi import FastAPI, status as fastapi_status
from pydantic import BaseModel, Field, create_model as create_pydantic_model
import uvicorn
# see also:
# https://fastapi.tiangolo.com/tutorial/response-model
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: list[str] = []
items = [
{
'name': 'Portal Gun',
'price': 42.0
},
{
'name': 'Plumbus',
'price': 32.0
},
]
class ListMetaResponse(BaseModel):
page: int = Field(..., title='Pagination page number', example='2')
per_page: int = Field(..., title='Pagination items per page', example='10')
count: int = Field(..., title='Number of items returned', example='10')
total: int = Field(..., title='Total number of items', example='100')
class ListResponse(BaseModel):
def __init__(self, data_model=None):
pass
# KeyError when using the Pydantic model dynamically created by created_model in two Generic Model as response model #3464
# https://github.com/tiangolo/fastapi/issues/3464
@lru_cache(None)
def __new__(cls, data_model=None):
if hasattr(data_model, '__name__'):
data_model_name = data_model.__name__
else:
data_model_name = '???'
print(f'data_model_name = {data_model_name}')
return create_pydantic_model(
data_model_name + 'ListResponse',
meta=(ListMetaResponse, ...),
data=(List[data_model], ...),
__base__=BaseModel,
)
app = FastAPI()
@app.get('/')
def index():
return 'Hello index'
@app.get(
'/items',
name='Get items',
status_code=fastapi_status.HTTP_200_OK,
response_model=ListResponse(Item)
)
async def return_items() -> Any:
return {
'meta': ListMetaResponse(
page=1,
per_page=10,
count=len(items),
total=len(items),
),
'data': items
}
@app.get(
'/items-values',
name='Get item values',
status_code=fastapi_status.HTTP_200_OK,
response_model=ListResponse(Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]),
)
async def return_items_values() -> Any:
# extract values from items
def get_item_values(item):
d = {
'name': None,
'description': None,
'price': None,
'tax': None,
'tags': None,
} | item
return tuple(d.values())
items_values = list(map(get_item_values, items))
print(f'items_values = {items_values}')
return {
'meta': ListMetaResponse(
page=1,
per_page=10,
count=len(items),
total=len(items),
),
'data': items_values,
}
if __name__ == "__main__":
uvicorn.run("main:app", host='127.0.0.1', port=8888, reload=True)
Résumé
La réduction de la quantité de données à transférer rend votre application plus efficace et plus réactive. Python La saisie ne prend pas en charge les éléments positionnels dans une liste, ce qui signifie que nous utilisons tuples. Dans de nombreux cas, comme la sélection de données dans une base de données, nous n'avons pas besoin d'une opération pour extraire les valeurs, car les lignes renvoyées sont déjà tuples.
Liens / crédits
FastAPI - Response Model - Return Type
https://fastapi.tiangolo.com/tutorial/response-model
Pydantic
https://docs.pydantic.dev/latest
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow