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

Devuelve sólo los valores de una lista de registros de FastAPI

Al devolver sólo valores en lugar de diccionarios, minimizamos el tamaño y el tiempo de transferencia de datos.

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

En Python, todo es una clase, lo que significa que los datos del modelo son similares a un diccionario. Pero los diccionarios tienen claves. Y cuando devuelves una lista de muchos diccionarios desde FastAPI, el tamaño de los datos, claves y valores, suele ser mucho más del doble del tamaño de los valores. Mayor tamaño y más tiempo significa que nuestra aplicación no es muy eficiente, más lenta de lo necesario. También significa que consume más energía, lo que significa que no es muy sostenible (suena bien ... ugh).

A continuación presento y comparo dos formas de devolver datos de FastAPI:

  • Una lista de diccionarios
  • Una lista de tuples que contiene valores de diccionario

Para hacer las cosas más emocionantes, la respuesta consta de una parte "meta" y una parte "data". Puedes probarlo tú mismo, el código está más abajo. Como siempre, estoy corriendo en Ubuntu 22.04.

La clase ListResponse

Ya he mencionado la clase ListResponse en un post anterior. Lo que queremos devolver son dos elementos, 'meta' y 'data'.

    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=count,
            total=total,
        ),
        'data': <list-of-dicts or list-of-tuples>,
    }

La clase ListResponse crea un único objeto de respuesta, utilizando la clase ListMetaResponse y el modelo de datos. Para utilizarla:

    response_model=ListResponse(Item)

1. Devolver una lista de diccionarios

Consulte el documento FastAPI 'Modelo de respuesta - Tipo de devolución' para conocer el modelo, véanse los enlaces siguientes.
Estamos utilizando el modelo y los elementos siguientes:

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
    },
]

Devolver los datos es muy sencillo; no te molestaré con esto aquí.

2. Devolver una lista de tuples que contenga los valores

En este caso utilizamos un tuple para los valores de un diccionario. No podemos utilizar una lista porque no hay soporte Python Typing para elementos posicionales en una lista. Los valores del diccionario deben colocarse en posiciones fijas en el tuple. Esto significa que el modelo es, véase más arriba:

Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]

Para obtener los valores en el tuples depende de su aplicación, de donde vienen los datos. Aquí asumo los 'items' como se muestra arriba, estos son diccionarios 'incompletos'. Podemos procesar esto a una lista de tuples de la siguiente manera:

    # 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 los "elementos" proceden de una base de datos, puede seleccionar todos los campos de la consulta. El resultado será una lista de tuples y no será necesario este procesamiento.

Comparación de los datos JSON

1. Lista de diccionarios

Para obtener los datos de JSON , ejecútelos en otro terminal:

curl http://127.0.0.1:8888/items

Resultado:

{"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":[]}]}

Sólo la parte de datos:

[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]

Número de bytes de los datos: 148.

2. Lista de tuples que contienen los valores

Para obtener los datos JSON :

curl http://127.0.0.1:8888/items-values

Resultado:

{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]}

Sólo la parte de datos:

[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]

Número de bytes de los datos: 68.

Esto supone una reducción del 54%.

El código

A continuación se muestra el código en caso de que quiera probar. Crear un virtual environment, entonces:

pip install fastapi
pip install uvicorn

Inicia la aplicación:

python main.py

Para mostrar los elementos, escriba en su navegador:

http://127.0.0.1:8888/items

Para mostrar los ítems-valores, escribe en tu navegador:

http://127.0.0.1:8888/items-values

El código:

# 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)

Resumen

Reducir la cantidad de datos a transferir hace que tu aplicación sea más eficiente y receptiva. Python Typing no admite elementos posicionales en una lista, lo que significa que utilizamos tuples. En muchos casos, como al seleccionar datos de una base de datos, no necesitamos una operación para extraer los valores, porque las filas devueltas ya son tuples.

Enlaces / créditos

FastAPI - Response Model - Return Type
https://fastapi.tiangolo.com/tutorial/response-model

Pydantic
https://docs.pydantic.dev/latest

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.