Aller au contenu

Événements de cycle de vie

🌐 Traduction par IA et humains

Cette traduction a été réalisée par une IA guidée par des humains. 🤝

Elle peut contenir des erreurs d'interprétation du sens original, ou paraître peu naturelle, etc. 🤖

Vous pouvez améliorer cette traduction en nous aidant à mieux guider le LLM d'IA.

Version anglaise

Vous pouvez définir une logique (du code) qui doit être exécutée avant que l'application ne démarre. Cela signifie que ce code sera exécuté une seule fois, avant que l'application ne commence à recevoir des requêtes.

De la même manière, vous pouvez définir une logique (du code) qui doit être exécutée lorsque l'application s'arrête. Dans ce cas, ce code sera exécuté une seule fois, après avoir traité potentiellement de nombreuses requêtes.

Comme ce code est exécuté avant que l'application ne commence à recevoir des requêtes, et juste après qu'elle termine de les traiter, il couvre tout le cycle de vie de l'application (le mot « lifespan » va être important dans un instant 😉).

Cela peut être très utile pour configurer des ressources dont vous avez besoin pour l'ensemble de l'application, qui sont partagées entre les requêtes, et/ou que vous devez nettoyer ensuite. Par exemple, un pool de connexions à une base de données, ou le chargement d'un modèle d'apprentissage automatique partagé.

Cas d'utilisation

Commençons par un exemple de cas d'utilisation, puis voyons comment le résoudre avec ceci.

Imaginons que vous ayez des modèles d'apprentissage automatique que vous souhaitez utiliser pour traiter des requêtes. 🤖

Les mêmes modèles sont partagés entre les requêtes, ce n'est donc pas un modèle par requête, ni un par utilisateur, ou quelque chose de similaire.

Imaginons que le chargement du modèle puisse prendre pas mal de temps, car il doit lire beaucoup de données depuis le disque. Vous ne voulez donc pas le faire pour chaque requête.

Vous pourriez le charger au niveau supérieur du module/fichier, mais cela signifierait aussi qu'il chargerait le modèle même si vous exécutez simplement un test automatisé simple ; ce test serait alors lent car il devrait attendre le chargement du modèle avant de pouvoir exécuter une partie indépendante du code.

C'est ce que nous allons résoudre : chargeons le modèle avant que les requêtes ne soient traitées, mais seulement juste avant que l'application ne commence à recevoir des requêtes, pas pendant le chargement du code.

Cycle de vie

Vous pouvez définir cette logique de démarrage et d'arrêt en utilisant le paramètre lifespan de l'application FastAPI, et un « gestionnaire de contexte » (je vais vous montrer ce que c'est dans un instant).

Commençons par un exemple, puis voyons-le en détail.

Nous créons une fonction async lifespan() avec yield comme ceci :

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

Ici, nous simulons l'opération de démarrage coûteuse de chargement du modèle en plaçant la fonction (factice) du modèle dans le dictionnaire avec les modèles d'apprentissage automatique avant le yield. Ce code sera exécuté avant que l'application ne commence à recevoir des requêtes, pendant le démarrage.

Puis, juste après le yield, nous déchargeons le modèle. Ce code sera exécuté après que l'application a fini de traiter les requêtes, juste avant l'arrêt. Cela pourrait, par exemple, libérer des ressources comme la mémoire ou un GPU.

Astuce

L’« arrêt » se produit lorsque vous arrêtez l'application.

Peut-être devez-vous démarrer une nouvelle version, ou vous en avez simplement assez de l'exécuter. 🤷

Fonction de cycle de vie

La première chose à remarquer est que nous définissons une fonction async avec yield. C'est très similaire aux Dépendances avec yield.

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

La première partie de la fonction, avant le yield, sera exécutée avant le démarrage de l'application.

Et la partie après le yield sera exécutée après que l'application a terminé.

Gestionnaire de contexte asynchrone

Si vous regardez, la fonction est décorée avec @asynccontextmanager.

Cela convertit la fonction en quelque chose appelé un « gestionnaire de contexte asynchrone ».

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

Un gestionnaire de contexte en Python est quelque chose que vous pouvez utiliser dans une instruction with. Par exemple, open() peut être utilisé comme gestionnaire de contexte :

with open("file.txt") as file:
    file.read()

Dans les versions récentes de Python, il existe aussi un gestionnaire de contexte asynchrone. Vous l'utiliseriez avec async with :

async with lifespan(app):
    await do_stuff()

Quand vous créez un gestionnaire de contexte ou un gestionnaire de contexte asynchrone comme ci-dessus, ce qu'il fait, c'est qu'avant d'entrer dans le bloc with, il exécute le code avant le yield, et après être sorti du bloc with, il exécute le code après le yield.

Dans notre exemple de code ci-dessus, nous ne l'utilisons pas directement, mais nous le transmettons à FastAPI pour qu'il l'utilise.

Le paramètre lifespan de l'application FastAPI accepte un gestionnaire de contexte asynchrone, nous pouvons donc lui passer notre nouveau gestionnaire de contexte asynchrone lifespan.

from contextlib import asynccontextmanager

from fastapi import FastAPI


def fake_answer_to_everything_ml_model(x: float):
    return x * 42


ml_models = {}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load the ML model
    ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model
    yield
    # Clean up the ML models and release the resources
    ml_models.clear()


app = FastAPI(lifespan=lifespan)


@app.get("/predict")
async def predict(x: float):
    result = ml_models["answer_to_everything"](x)
    return {"result": result}

Événements alternatifs (déprécié)

Alertes

La méthode recommandée pour gérer le démarrage et l'arrêt est d'utiliser le paramètre lifespan de l'application FastAPI comme décrit ci-dessus. Si vous fournissez un paramètre lifespan, les gestionnaires d'événements startup et shutdown ne seront plus appelés. C'est soit tout en lifespan, soit tout en événements, pas les deux.

Vous pouvez probablement passer cette partie.

Il existe une autre manière de définir cette logique à exécuter au démarrage et à l'arrêt.

Vous pouvez définir des gestionnaires d'événements (fonctions) qui doivent être exécutés avant le démarrage de l'application, ou lorsque l'application s'arrête.

Ces fonctions peuvent être déclarées avec async def ou un def normal.

Événement startup

Pour ajouter une fonction qui doit être exécutée avant le démarrage de l'application, déclarez-la avec l'événement « startup » :

from fastapi import FastAPI

app = FastAPI()

items = {}


@app.on_event("startup")
async def startup_event():
    items["foo"] = {"name": "Fighters"}
    items["bar"] = {"name": "Tenders"}


@app.get("/items/{item_id}")
async def read_items(item_id: str):
    return items[item_id]

Dans ce cas, la fonction gestionnaire de l'événement startup initialisera la « base de données » des items (juste un dict) avec quelques valeurs.

Vous pouvez ajouter plusieurs fonctions de gestion d'événements.

Et votre application ne commencera pas à recevoir des requêtes avant que tous les gestionnaires de l'événement startup aient terminé.

Événement shutdown

Pour ajouter une fonction qui doit être exécutée lorsque l'application s'arrête, déclarez-la avec l'événement « shutdown » :

from fastapi import FastAPI

app = FastAPI()


@app.on_event("shutdown")
def shutdown_event():
    with open("log.txt", mode="a") as log:
        log.write("Application shutdown")


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]

Ici, la fonction gestionnaire de l'événement shutdown écrira une ligne de texte « Application shutdown » dans un fichier log.txt.

Info

Dans la fonction open(), le mode="a" signifie « append » (ajouter) ; la ligne sera donc ajoutée après ce qui se trouve déjà dans ce fichier, sans écraser le contenu précédent.

Astuce

Notez que dans ce cas, nous utilisons une fonction Python standard open() qui interagit avec un fichier.

Cela implique des E/S (input/output), qui nécessitent « d'attendre » que des choses soient écrites sur le disque.

Mais open() n'utilise pas async et await.

Nous déclarons donc la fonction gestionnaire d'événement avec un def standard plutôt qu'avec async def.

startup et shutdown ensemble

Il y a de fortes chances que la logique de votre démarrage et de votre arrêt soit liée : vous pourriez vouloir démarrer quelque chose puis le terminer, acquérir une ressource puis la libérer, etc.

Faire cela dans des fonctions séparées qui ne partagent pas de logique ni de variables est plus difficile, car vous devriez stocker des valeurs dans des variables globales ou recourir à des astuces similaires.

Pour cette raison, il est désormais recommandé d'utiliser plutôt le lifespan comme expliqué ci-dessus.

Détails techniques

Juste un détail technique pour les nerds curieux. 🤓

Sous le capot, dans la spécification technique ASGI, cela fait partie du protocole Lifespan, et il y définit des événements appelés startup et shutdown.

Info

Vous pouvez en lire plus sur les gestionnaires lifespan de Starlette dans la documentation « Lifespan » de Starlette.

Y compris comment gérer l'état de cycle de vie qui peut être utilisé dans d'autres parties de votre code.

Sous-applications

🚨 Gardez à l'esprit que ces événements de cycle de vie (démarrage et arrêt) ne seront exécutés que pour l'application principale, pas pour Sous-applications - Montages.