Guide de traduction · Python
De l'UML
au Code
Comment lire un diagramme UML, quand le produire, et traduire chaque notation en code Python propre et idiomatique.
Quand modéliser en UML ?
L'UML est un outil de communication, pas une fin en soi. La bonne question n'est pas "est-ce que je dois faire un diagramme ?" mais "est-ce qu'un diagramme va m'aider à avancer plus vite ?"
Le piège "Big Design Up Front" : ne pas passer des semaines à perfectionner des diagrammes avant de coder. L'UML doit aider à démarrer, pas à procrastiner. Un diagramme de 30 minutes qui clarifie une architecture vaut mieux qu'une semaine de diagrammes exhaustifs qui seront tous obsolètes au premier sprint.
Workflow UML → Code
Ordre de traduction recommandé pour partir d'un diagramme et arriver à un code Python propre.
Règle d'or : traduire dans l'ordre ①→⑤. Les use cases donnent la liste des classes candidates. Les classes définissent la structure. Les séquences remplissent les méthodes. Les tests valident les cas d'utilisation. Si le code ne colle pas avec le diagramme → c'est souvent le diagramme qui a tort, pas le code.
Diagramme de Classes → Python
Correspondance exhaustive entre chaque notation du diagramme de classes et sa traduction Python.
Nom →
class Nom:- attr → self.__attr (privé)# attr → self._attr (protégé)+ attr → self.attr (public)= valeur → valeur par défaut dans __init__
class Voiture:
def __init__(self, marque: str, id: int):
self.__marque = marque # - privé
self.__vitesse: int = 0 # - privé, défaut 0
self._id = id # # protégé
def accelerer(self, v: int) -> None: # +
if self.__valider_vitesse():
self.__vitesse += v
def __valider_vitesse(self) -> bool: # -
return self.__vitesse < 300
«abstract» ou nom en italique → ABCMéthodes en italique →
@abstractmethodMéthodes normales → implémentation concrète
from abc import ABC, abstractmethod
class Forme(ABC): # «abstract»
def __init__(self, couleur: str):
self._couleur = couleur # # protégé
@abstractmethod
def aire(self) -> float: # méth. italique
pass
def getCouleur(self) -> str: # concrète
return self._couleur
# Forme() → TypeError (ne peut s'instancier)
# Cercle() → OK si aire() est définie
«interface» → ABC avec toutes les méthodes @abstractmethodPas d'attributs d'instance
Relation de réalisation → héritage Python
from abc import ABC, abstractmethod
class Serialisable(ABC): # «interface»
@abstractmethod
def serialiser(self) -> str: ...
@abstractmethod
def deserialiser(self, s: str): ...
# Réalisation ──────────────────────────
class Document(Serialisable): # «realize»
def serialiser(self) -> str:
return "..."
def deserialiser(self, s: str):
pass
class Compteur:
nb_instances: int = 0 # statique (souligné)
def __init__(self):
Compteur.nb_instances += 1
@classmethod # méthode statique
def getNb(cls) -> int:
return cls.nb_instances
# Alternative avec @staticmethod
# si pas d'accès à cls/self
«enumeration»
Classesfrom enum import Enum
class Couleur(Enum):
ROUGE = 1
VERT = 2
BLEU = 3«singleton»
Classesclass Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance«dataType»
Classesfrom dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
z: float = 0.0«utility»
Classes# math_utils.py
def clamper(v, mn, mx):
return max(mn, min(v, mx))
def arrondir(v, n=2):
return round(v, n)Relations → Python
La relation UML dicte comment les objets se connaissent et se créent. C'est souvent la partie la plus délicate à traduire.
Sous-classe remplace ou étend le parent
Appeler
super().__init__() toujoursPréférer la composition si "est un" n'est pas naturel
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, nom: str):
self.nom = nom
@abstractmethod
def parler(self) -> str: ...
class Chien(Animal): # ──▷ Animal
def __init__(self, nom: str):
super().__init__(nom) # toujours !
def parler(self) -> str:
return "Wouf"
class Chat(Animal): # ──▷ Animal
def parler(self) -> str:
return "Miaou"
__init__ du tout.Leur durée de vie est liée : si
Maison est détruite, les Pièce le sont aussi.→ créer dans
__init__, pas reçu en paramètre
class Piece:
def __init__(self, nom: str):
self.nom = nom
class Maison: # ◆── Pièce
def __init__(self, nb_pieces: int):
# Créées ICI → composition
self.pieces = [
Piece(f"Pièce {i+1}")
for i in range(nb_pieces)
]
# Quand maison = None → pieces perdues aussi
Ils existent indépendamment du tout.
→ reçu en paramètre dans
__init__ ou via méthode
class Joueur:
def __init__(self, nom: str):
self.nom = nom
class Equipe: # ◇── Joueur
def __init__(self):
self.joueurs: list[Joueur] = []
def ajouter(self, j: Joueur):
self.joueurs.append(j) # reçu !
# Equipe dissoute → joueurs survivent
| Multiplicité UML | Type Python | Exemple |
|---|---|---|
| 1 | Objet direct | self.adresse = a |
| 0..1 | Optional[T] | self.adresse: Optional[Adresse] = None |
| * | list[T] | self.commandes: list[Commande] = [] |
| 1..* | list[T] non vide | Vérifier len ≥ 1 |
| Ordonné | list[T] | Ordre garanti |
| Non ordonné unique | set[T] | Pas de doublons |
| Clé → valeur | dict[K, V] | Association qualifiée |
from __future__ import annotations
from typing import Optional
class Client:
def __init__(self, nom: str):
self.nom = nom
# 1 → objet direct
self.compte: Compte
# 0..1 → Optional
self.fidélité: Optional[CarteF] = None
# * → liste
self.commandes: list[Commande] = []
# non ord. unique → set
self.tags: set[str] = set()
Dépendance = l'objet dépendant peut changer si la classe cible change. C'est la relation la plus faible et la plus courante (import d'un module, paramètre de méthode).
class Rapport:
# Rapport - -▶ Formateur
# Formateur utilisé seulement dans generer()
# → pas d'attribut self.formateur !
def generer(self, fmt: Formateur) -> str:
return fmt.formater(self.contenu)
def exporter(self) -> None:
# usage local seulement
logger = Logger()
logger.log("export")
Cas d'utilisation → Architecture du Code
Le diagramme de cas d'utilisation ne se traduit pas en classes directement — il structure votre architecture et pilote vos tests.
Acteur principal
Use CasesCas d'utilisation
Use Cases«include»
Use Casesdef passer_commande(self):
self.s_authentifier() # include
# ... logique commande«extend»
Use Casesdef retourner_livre(self):
# ... logique retour
if self.est_en_retard():
self.payer_amende() # extend1 scénario = 1 test. Le scénario principal, chaque scénario alternatif et chaque cas d'erreur sont autant de cas de test.
import pytest
from auth import AuthService, AuthException
class TestSeConnecter:
# Scénario principal ─────────────────
def test_identifiants_valides(self, svc):
token = svc.connecter("alice", "pass")
assert token is not None
assert len(token) > 0
# Scénario alternatif ────────────────
def test_mauvais_mot_de_passe(self, svc):
with pytest.raises(AuthException):
svc.connecter("alice", "faux")
# Précondition ────────────────────────
def test_utilisateur_inconnu(self, svc):
with pytest.raises(AuthException):
svc.connecter("inconnu", "x")
Diagramme de Séquence → Python
Le diagramme de séquence se traduit directement en corps de méthodes. Chaque message est un appel.
Message synchrone
Message asynchrone
Fragment alt
Fragment loop
«create»
Auto-message
class AuthController:
def __init__(self):
self.svc = UserService() # create
def connecter(self, user, pw) -> str:
# msg 1 : valider(user, pass)
return self.svc.valider(user, pw)
class UserService:
def __init__(self):
self.db = Database()
def valider(self, user, pw) -> str:
# alt [valide] ──────────────────
u = self.db.findUser(user) # msg2
if u and u.check(pw): # [valide]
return generer_token(u) # retour
# alt [invalide] ─────────────────
raise AuthException( # retour
"Identifiants invalides"
)
Patterns UML → Code fréquents
Combinaisons de notations UML qui correspondent à des design patterns reconnaissables.
Factory Method
class CreateurDoc(ABC):
@abstractmethod
def creer(self) -> Document: ...
class CreateurPDF(CreateurDoc):
def creer(self) -> Document:
return DocumentPDF()Observer
class Sujet:
def __init__(self):
self._obs: list[Observer] = []
def notifier(self):
for o in self._obs:
o.update(self)Strategy
class Trieur:
def __init__(self, algo: Algo):
self._algo = algo # agrégation
def trier(self, data):
return self._algo.executer(data)Facade
class Facade:
def __init__(self):
self._a = SysA() # composition
self._b = SysB()
def operation(self):
self._a.init(); self._b.run()Anti-patterns — Erreurs fréquentes
Erreurs courantes lors de la traduction UML → Code, et comment les corriger.
class Maison:
# ◆── Pièce, mais la pièce est reçue
# en paramètre → c'est une agrégation !
def __init__(self, piece: Piece):
self.piece = piece # ← FAUXclass Maison:
def __init__(self):
self.piece = Piece() # créée ici ✓class Chien(Animal):
def __init__(self, nom):
# Oubli de super().__init__(nom) !
self.race = "labrador"class Chien(Animal):
def __init__(self, nom):
super().__init__(nom) # ✓
self.race = "labrador"# «interface» mais avec attribut d'instance
class Serialisable(ABC):
def __init__(self):
self.format = "json" # ← interditclass Serialisable(ABC):
# Aucun __init__, aucun attribut
@abstractmethod
def serialiser(self) -> str: ... # ✓# UML : Client ──* Commande
# mais codé comme 0..1 !
class Client:
def __init__(self):
self.commande = None # ← 0..1 ?!class Client:
def __init__(self):
self.commandes: list[Commande] = [] # * ✓Checklist avant de coder
À vérifier sur votre diagramme avant de créer le premier fichier Python.
📐 Diagramme de classes
🎭 Use Cases & Séquences
Exemple de A à Z — Système de Commandes
Un mini-projet complet : de l'énoncé aux 3 diagrammes, jusqu'au code Python final.
from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional
# ── Classe abstraite Personne ──────────────────────────────────────────
class Personne(ABC): # «abstract», nom en italique
def __init__(self, nom: str, email: str):
self._nom = nom # # protégé
self._email = email # # protégé
# ── LigneCommande (dataType) ───────────────────────────────────────────
class LigneCommande: # partie de Commande (composition)
def __init__(self, article: str, qte: int, prix: float):
self.article = article
self.qte = qte
self.prix = prix
def sous_total(self) -> float:
return self.qte * self.prix
# ── Commande ───────────────────────────────────────────────────────────
class Commande:
def __init__(self):
self.__date = datetime.now() # - privé
self.__statut = "attente" # - privé, défaut
# Composition ◆── LigneCommande (créées ici, 1..*)
self.__lignes: list[LigneCommande] = []
def ajouter_ligne(self, article: str, qte: int, prix: float):
self.__lignes.append(LigneCommande(article, qte, prix)) # composition
def calculerTotal(self) -> float: # + public
return sum(l.sous_total() for l in self.__lignes)
# ── Client ──────────────────────────────────────────────────────────────
class Client(Personne): # héritage ──▷ Personne
def __init__(self, nom: str, email: str, adresse: str):
super().__init__(nom, email) # toujours !
self.__adresse = adresse
# Association ──▶ Commande (multiplicité *)
self.__commandes: list[Commande] = []
def passerCommande(self) -> Commande: # + use case
cmd = Commande()
self.__commandes.append(cmd)
return cmd
def consulterCommandes(self) -> list[Commande]: # + use case
return list(self.__commandes)
# ── Livreur ──────────────────────────────────────────────────────────────
class Livreur(Personne): # héritage ──▷ Personne
def __init__(self, nom: str, email: str, vehicule: str):
super().__init__(nom, email)
self.__vehicule = vehicule
def livrer(self, c: Commande) -> None: # + use case
# Dépendance - -▶ Commande (usage temporaire)
c._Commande__statut = "livré"
# ── Utilisation ──────────────────────────────────────────────────────────
if __name__ == "__main__":
alice = Client("Alice", "alice@ex.com", "Paris")
livreur = Livreur("Bob", "bob@ex.com", "vélo")
cmd = alice.passerCommande()
cmd.ajouter_ligne("livre", 2, 15.0)
cmd.ajouter_ligne("stylo", 3, 1.5)
print(f"Total : {cmd.calculerTotal()}€") # 34.5€
livreur.livrer(cmd)
print(alice.consulterCommandes()) # [cmd]