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.
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
Recientes
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
- Docker Swarm rolling updates
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb