Typer les comportements variables
Objectif : Exprimer des interfaces acceptant plusieurs implémentations sans perdre la vérification statique.
Dans un outil de dépenses, plusieurs sources peuvent fournir des dépenses : un fichier, une base de données ou un service distant. Si votre logique métier exige une classe précise, remplacer une source devient coûteux et les dépendances se resserrent. Vous allez décrire le comportement attendu plutôt que l’identité de la classe, puis écrire une fonction réutilisable qui conserve le type des éléments reçus.
Décrire un comportement avec Protocol
Jusqu’ici, les modèles de depenses.modeles décrivent les données et leurs conversions. Un Protocol complète ces contrats en décrivant les opérations qu’un objet doit proposer. Contrairement à une classe de base, il n’est pas nécessaire que les différentes implémentations héritent explicitement du protocole : elles doivent simplement fournir les attributs ou méthodes attendus.
Pour le service, la dépense reste le tuple Depense défini dans modeles.py. L’exemple suivant place dans service.py un protocole de source et une fonction d’agrégation. La source doit fournir une méthode lister qui renvoie des dépenses ; le service n’a pas besoin de connaître son mode de stockage.
from decimal import Decimal
from typing import Protocol
from depenses.modeles import Depense, total_des_depenses
class SourceDepenses(Protocol):
def lister(self) -> list[Depense]:
"""Fournit les dépenses disponibles."""
...
def total_depuis_source(source: SourceDepenses) -> Decimal:
return total_des_depenses(source.lister())
class SourceMemoire:
def __init__(self, depenses: list[Depense]) -> None:
self._depenses = depenses
def lister(self) -> list[Depense]:
return self._depenses
if __name__ == "__main__":
source = SourceMemoire([
("Repas", Decimal("18.40"), None),
("Transport", Decimal("6.20"), None),
])
print(total_depuis_source(source))24.60SourceMemoire n’hérite pas de SourceDepenses, mais respecte son contrat en exposant lister avec la signature attendue. Une autre classe pourrait lire les données ailleurs sans modifier total_depuis_source. Le vérificateur statique pourra signaler une source dont la méthode est absente ou dont le résultat ne correspond pas au type annoncé.
Réutiliser une fonction avec TypeVar
La fonction précédente accepte une source de dépenses, mais son protocole est volontairement spécialisé. Pour une opération réutilisable — par exemple sélectionner des éléments répondant à un critère — vous pouvez définir un paramètre de type avec TypeVar. Ce paramètre représente un type déterminé à l’appel : la fonction accepte alors différents types d’éléments tout en restituant exactement le type reçu.
from typing import Callable, TypeVar
T = TypeVar("T")
def selectionner(
elements: list[T],
critere: Callable[[T], bool],
) -> list[T]:
return [element for element in elements if critere(element)]
# Exemple avec les tuples Depense de modeles.py
from decimal import Decimal
from depenses.modeles import Depense
depenses: list[Depense] = [
("Repas", Decimal("18.40"), None),
("Transport", Decimal("6.20"), None),
]
repas = selectionner(depenses, lambda depense: depense[0] == "Repas")
print(repas)[('Repas', Decimal('18.40'), None)]Ici, T est déduit comme le type Depense à partir de la liste fournie. Le critère reçoit donc une dépense, et le résultat est une liste de ces mêmes dépenses, pas une liste d’objets quelconques. La fonction s’applique aussi à une liste d’autres types, à condition que le critère traite correctement ces éléments.
Associer génériques et protocoles
Un protocole peut lui-même être générique : vous pouvez ainsi décrire un critère applicable à un type donné sans figer ce type dans le contrat. Dans les projets simples, Callable suffit souvent pour exprimer une fonction qui reçoit un élément et renvoie un booléen, comme dans l’exemple. Utilisez un protocole générique lorsque le comportement doit être nommé, partagé ou enrichi de plusieurs méthodes.
Vérifiez vos choix avec les noms réellement définis dans modeles.py et gardez les conversions explicites aux frontières des modules. Les outils de vérification statique permettront ensuite de contrôler ces contrats sur le projet entier ; leur configuration et l’interprétation de leurs diagnostics seront traitées dans une leçon ultérieure.
À retenir
- Un Protocol formalise les opérations attendues sans obliger les implémentations à hériter d’une classe commune.
- Décrivez dans un protocole les besoins réels du consommateur, comme la méthode lister utilisée par le service.
- TypeVar permet à une fonction générique de traiter plusieurs types tout en conservant le type de ses éléments en entrée et en sortie.
- Callable convient aux critères simples ; un protocole générique devient pertinent pour un comportement nommé ou composé.
La prochaine leçon configure la vérification statique pour contrôler ces contrats dans le projet.