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 ?"

Phase 1 — Analyse des besoins
Diagramme de cas d'utilisation
Avant d'écrire la moindre ligne de code. Cadre le périmètre avec le client. Identifie les acteurs et les fonctionnalités. Évite de construire ce dont personne n'a besoin.
Use CasesObligatoire si équipe > 1Partagé avec le client
Phase 2 — Conception architecturale
Diagramme de classes (haut niveau)
Avant de coder les entités principales. Identifie les classes clés, leurs responsabilités et les grandes relations. Ne pas encore inclure tous les attributs — c'est une esquisse.
Classes~30 min maxPapier ou whiteboard
Phase 3 — Conception détaillée
Diagramme de classes complet + Séquences des scénarios critiques
Pour les interactions complexes uniquement. Documenter le scénario principal et les 1–2 scénarios alternatifs les plus importants. Pas besoin de modéliser chaque méthode.
ClassesSéquenceScénarios critiques seulement
Phase 4 — Implémentation
Le code devient la référence — les diagrammes suivent
Ne pas maintenir les diagrammes en temps réel — c'est trop coûteux. Les regénérer (ou les mettre à jour) lors des revues d'architecture, avant des refactorings importants, ou pour l'onboarding d'un nouveau développeur.
Revue archiAvant refactoringOnboarding
Phase 5 — Maintenance
Rétro-ingénierie : Code → UML pour comprendre l'existant
Quand on reprend un projet sans documentation. Construire un diagramme de classes depuis le code existant permet de visualiser les dépendances, détecter les couplages excessifs et planifier des refactorings.
Rétro-ingénierieRéduction de dette technique
⚠️

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.

① Use Cases Modules / classes candidates ② Classes Squelettes de classes Python ③ Relations Héritage, compo, agrégation ④ Séquences Corps des méthodes ⑤ Tests Un test par cas d'utilisation ⑥ Itérer Ajuster le diagramme Ordre de traduction recommandé
💡

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.

Classe concrète
UML
Voiture - marque : String - vitesse : int = 0 # id : int + accelerer(v:int) : void - validerVitesse() : bool
Règles de lecture :
Nom → class Nom:
- attrself.__attr (privé)
# attrself._attr (protégé)
+ attrself.attr (public)
= valeur → valeur par défaut dans __init__
Python
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
Classe abstraite
UML
«abstract» Forme # couleur : String + aire() : float + getCouleur() : String
Règles :
«abstract» ou nom en italique → ABC
Méthodes en italique → @abstractmethod
Méthodes normales → implémentation concrète
Python
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
UML
«interface» Serialisable + serialiser() : str + deserialiser(s:str)
Règles :
«interface» → ABC avec toutes les méthodes @abstractmethod
Pas d'attributs d'instance
Relation de réalisation → héritage Python
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
Membres statiques
UML — membres soulignés
Compteur + nbInstances : int = 0 ← statique + getNb() : int ← statique
Python
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
Stéréotypes → Patterns Python
🔢

«enumeration»

Classes
«enumeration»class C(Enum)
ConstanteNOM = valeur
from enum import Enum
class Couleur(Enum):
    ROUGE = 1
    VERT  = 2
    BLEU  = 3
🔒

«singleton»

Classes
«singleton»__new__ override
- instance_instance = None
class Config:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
📦

«dataType»

Classes
«dataType»@dataclass
attributs seulsfields typés
from dataclasses import dataclass
@dataclass
class Point:
    x: float
    y: float
    z: float = 0.0
🔧

«utility»

Classes
«utility»module Python
méth. statiquesfonctions simples
# 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.

Héritage — "est un"
UML — triangle creux ——▷
Animal + parler() : str Chien Chat
Points clés :
Sous-classe remplace ou étend le parent
Appeler super().__init__() toujours
Préférer la composition si "est un" n'est pas naturel
Python
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"
Composition — "composé de" ◆
UML — losange plein ◆——
Maison Pièce 1 1..*
Règle clé : les objets "partie" sont créés dans __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
Python
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
Agrégation — "a un" ◇
UML — losange vide ◇——
Equipe Joueur 1 *
Règle clé : les objets "partie" sont reçus en paramètre ou ajoutés plus tard.
Ils existent indépendamment du tout.
→ reçu en paramètre dans __init__ ou via méthode
Python
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
Association & Multiplicités
UML — flèche simple ——▶ avec multiplicités
La multiplicité cible détermine le type Python à utiliser.
Multiplicité UMLType PythonExemple
1Objet directself.adresse = a
0..1Optional[T]self.adresse: Optional[Adresse] = None
*list[T]self.commandes: list[Commande] = []
1..*list[T] non videVérifier len ≥ 1
Ordonnélist[T]Ordre garanti
Non ordonné uniqueset[T]Pas de doublons
Clé → valeurdict[K, V]Association qualifiée
Python
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 — usage temporaire - -▶
UML — flèche pointillée - -▶
La classe ne stocke pas de référence permanente — elle utilise l'autre classe localement, souvent en paramètre de méthode.
📌

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).

Python
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 Cases
ActeurModule / point d'entrée
Héritage acteursRôles / permissions
«système»Classe cliente / API
📦

Cas d'utilisation

Use Cases
Cas UCMéthode publique
Préconditionassert / guard clause
Postconditionvaleur retournée
Scénario alt.Exception / if
🔗

«include»

Use Cases
«include»Appel de méthode
ObligatoirePas de condition
def passer_commande(self):
    self.s_authentifier()  # include
    # ... logique commande

«extend»

Use Cases
«extend»if conditionnel
OptionnelGuard clause
def retourner_livre(self):
    # ... logique retour
    if self.est_en_retard():
        self.payer_amende()  # extend
Use Case → Test unitaire
Fiche Use Case
UC : Se connecter
PréconditionUtilisateur enregistré
Scénario OKIdentifiants valides → token
Scénario KOIdentifiants invalides → AuthException
💡

1 scénario = 1 test. Le scénario principal, chaque scénario alternatif et chaque cas d'erreur sont autant de cas de test.

Python — pytest
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

A→B:methode()b.methode()
retour valx = b.methode()
pas de retourb.methode() seul

Message asynchrone

→ ouvertThread / asyncio
non bloquantasync def / await
fire & forgetasyncio.create_task
🔄

Fragment alt

alt [cond]if condition:
else (sépar.)else:
opt [cond]if condition: (seul)
🔁

Fragment loop

loop [cond]while condition:
loop(n,m)for i in range(n, m+1):
loop *for item in collection:

«create»

«create» :Bb = B(args)
dans __init__composition
dans méthodecréation locale
💬

Auto-message

A→A:calc()self.calc()
retour internevaleur locale
récursionappel récursif
Diagramme de séquence — Login
:Ctrl :UserService :Database valider(user,pass) alt [valide] findUser() user:User token:str [invalide] AuthException
Python
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

UML : Classe abstraite + méthode abstraite retournant un objet
class CreateurDoc(ABC):
    @abstractmethod
    def creer(self) -> Document: ...

class CreateurPDF(CreateurDoc):
    def creer(self) -> Document:
        return DocumentPDF()
👀

Observer

UML : agrégation vers interface Observer
class Sujet:
    def __init__(self):
        self._obs: list[Observer] = []
    def notifier(self):
        for o in self._obs:
            o.update(self)
🎨

Strategy

UML : agrégation vers interface Strategy
class Trieur:
    def __init__(self, algo: Algo):
        self._algo = algo      # agrégation
    def trier(self, data):
        return self._algo.executer(data)
🔌

Facade

UML : une classe avec dépendances vers N sous-systèmes
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.

❌ Composition codée comme agrégation
FAUX
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  # ← FAUX
CORRECT
class Maison:
    def __init__(self):
        self.piece = Piece()  # créée ici ✓
❌ Oublier super() dans l'héritage
FAUX
class Chien(Animal):
    def __init__(self, nom):
        # Oubli de super().__init__(nom) !
        self.race = "labrador"
CORRECT
class Chien(Animal):
    def __init__(self, nom):
        super().__init__(nom)  # ✓
        self.race = "labrador"
❌ Confondre interface et classe abstraite
FAUX
# «interface» mais avec attribut d'instance
class Serialisable(ABC):
    def __init__(self):
        self.format = "json"  # ← interdit
CORRECT
class Serialisable(ABC):
    # Aucun __init__, aucun attribut
    @abstractmethod
    def serialiser(self) -> str: ...  # ✓
❌ Ignorer les multiplicités
FAUX
# UML : Client ──* Commande
# mais codé comme 0..1 !
class Client:
    def __init__(self):
        self.commande = None  # ← 0..1 ?!
CORRECT
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

Chaque classe a un nom en PascalCase clair
Les visibilités (+/-/#) sont indiquées
Les multiplicités sont précisées sur toutes les relations
Composition vs agrégation distinguée (◆ vs ◇)
Les classes abstraites sont en italique ou «abstract»
Les méthodes abstraites sont en italique
Les stéréotypes sont cohérents (enum, singleton…)
Pas plus de 10 classes par diagramme (lisibilité)
Pas de cycles de composition (A ◆→ B ◆→ A)
Héritage non utilisé pour partager du code seulement

🎭 Use Cases & Séquences

Chaque UC a un verbe à l'infinitif
Les acteurs sont des rôles, pas des personnes
«include» vs «extend» correctement utilisé
Chaque UC critique a son diagramme de séquence
Les messages de séquence correspondent à des méthodes du DC
Les fragments alt couvrent tous les cas d'erreur
Séquence non utilisée pour les flux trivaux
Cohérence entre les 3 diagrammes
Pas de logique de présentation dans les 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.

1
Énoncé
Un client peut passer des commandes. Chaque commande contient des lignes. Un livreur gère les livraisons.
2
Use Cases identifiés
Passer commande · Consulter commandes · Gérer livraison
Système de Commandes Passer commande Consulter commandes Gérer livraison Client Livreur
3
Diagramme de classes
Personne (abstraite), Client, Livreur, Commande, LigneCommande
«abstract» Personne # nom : str # email : str Client - adresse : str + passerCommande() + consulterCommandes() Livreur - vehicule : str + livrer(c:Commande) Commande - date : Date - statut : str = "attente" + calculerTotal() : float LigneCommande 1 * 1..*
4
Code Python final — traduit du diagramme
commandes.py
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]