angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

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.

6 juillet 2023
Dans API, FastAPI
post main image
https://www.pexels.com/nl-nl/@hellokellybrito/

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

En savoir plus...

API FastAPI

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.