Leçon 2 / 20

Structurer les échanges entre modules

15 min Python — typage, tests et performance Gratuit

Objectif : Décrire les objets transmis entre les modules de l’outil de dépenses et convertir ces données en modèle métier.

Quand plusieurs modules manipulent une dépense, un tuple seul ne dit pas clairement ce que représente chaque position. Une catégorie reçue d’un fichier ou d’une interface peut aussi ne pas correspondre aux valeurs acceptées par le service. Vous allez expliciter ces échanges et séparer le format reçu du modèle métier.

La leçon précédente a défini les annotations et les types de base employés par le projet. Ici, vous allez donner une forme nommée aux données qui circulent entre les composants, puis convertir ces données vers Depense, le tuple métier déjà présent dans modeles.py.

Décrire les données reçues avec TypedDict

TypedDict décrit un dictionnaire dont les clés et les valeurs attendues sont connues. Il convient au format d’une requête ou d’un enregistrement importé, car les données restent des dictionnaires ordinaires à l’exécution. Ce type aide les outils d’analyse à signaler une clé oubliée ou une valeur d’un type inattendu, mais ne valide pas à lui seul des données externes.

src/depenses/modeles.pyTexte
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from typing import TypedDict


class Categorie(Enum):
    ALIMENTATION = "alimentation"
    TRANSPORT = "transport"
    LOGEMENT = "logement"
    AUTRE = "autre"


class Description(str):
    """Marqueur de type pour la description d'une dépense."""


class DepenseRecue(TypedDict):
    description: str
    montant: Decimal
    categorie: str | None


@dataclass(frozen=True)
class DepenseDetaillee:
    description: Description
    montant: Decimal
    categorie: Categorie | None


Depense = tuple[Description, Decimal, Categorie | None]


def creer_depense(
    description: str,
    montant: Decimal,
    categorie: Categorie | None = None,
) -> Depense:
    return (Description(description), montant, categorie)


def total_des_depenses(depenses: list[Depense]) -> Decimal:
    return sum((montant for _, montant, _ in depenses), start=Decimal("0"))


def depense_depuis_donnees(donnees: DepenseRecue) -> DepenseDetaillee:
    categorie_recue = donnees["categorie"]
    categorie = Categorie(categorie_recue) if categorie_recue is not None else None
    return DepenseDetaillee(
        description=Description(donnees["description"]),
        montant=donnees["montant"],
        categorie=categorie,
    )


def vers_depense_metier(depense: DepenseDetaillee) -> Depense:
    return (depense.description, depense.montant, depense.categorie)


def convertir_lots(donnees: list[DepenseRecue]) -> list[Depense]:
    return [
        vers_depense_metier(depense_depuis_donnees(item))
        for item in donnees
    ]


if __name__ == "__main__":
    donnees: DepenseRecue = {
        "description": "Train",
        "montant": Decimal("12.50"),
        "categorie": "transport",
    }
    depense = depense_depuis_donnees(donnees)
    print(depense)
    print(total_des_depenses([vers_depense_metier(depense)]))
Résultat attendu
DepenseDetaillee(description='Train', montant=Decimal('12.50'), categorie=<Categorie.TRANSPORT: 'transport'>)
12.50

Le schéma DepenseRecue décrit les clés réellement échangées : description, montant et catégorie éventuellement absente. La fonction depense_depuis_donnees transforme la chaîne reçue en membre de Categorie ; une valeur non reconnue déclenche une ValueError. Cette conversion est un point de contrôle explicite : le typage statique décrit les attentes, tandis que cette opération vérifie concrètement la catégorie.

Employer Enum et dataclass dans le modèle

Enum limite les catégories à un ensemble nommé : vous manipulez Categorie.TRANSPORT dans le code, et la valeur "transport" reste disponible pour les échanges. Une dataclass rassemble les champs d’un objet métier et génère les méthodes courantes de représentation et d’égalité. L’option frozen=True empêche de réaffecter ses champs après sa création.

Le projet conserve le tuple Depense comme contrat du service, conformément à l’état déjà établi dans la leçon précédente. DepenseDetaillee fournit une représentation nommée utile aux échanges et à la lecture ; vers_depense_metier effectue le passage vers le format attendu par total_des_depenses. Vous pouvez ainsi clarifier les responsabilités sans modifier les appels existants du service.

À retenir

  • TypedDict documente la forme et les types attendus d’un dictionnaire transmis entre modules.
  • Enum fournit des catégories nommées dont les valeurs sont contrôlées lors de la conversion.
  • dataclass regroupe les champs d’une dépense sous forme d’objet ; frozen=True évite leur réaffectation.
  • Convertissez explicitement les données reçues avant de les transmettre au service, qui conserve ici son tuple Depense.

Dans la suite, vous pourrez définir des comportements communs à plusieurs objets sans dépendre de leur classe concrète.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.