Debutant 16 min de lecture · 3 315 mots

Programmation orientée objet en Python : Classes et héritage (Partie 2)

Estimated reading time: 17 minutes

Projet pratique : Système de bibliothèque

"""
project/library.py - Système de gestion de bibliothèque complet
"""

from typing import List, Optional, Dict, Set
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from abc import ABC, abstractmethod
from enum import Enum
import json

class StatutEmprunt(Enum):
    """Statuts possibles d'un emprunt."""
    ENCOURS = "en cours"
    RETOURNE = "retourné"
    ENRETARD = "en retard"

class TypeDocument(Enum):
    """Types de documents disponibles."""
    LIVRE = "livre"
    MAGAZINE = "magazine"
    DVD = "dvd"
    CD = "cd"

# Classes de base
class Document(ABC):
    """
    Classe abstraite représentant un document de bibliothèque.

    Attributes:
        id: Identifiant unique
        titre: Titre du document
        disponible: Si le document est disponible
    """

    def init(self, iddoc: str, titre: str) -> None:
        """
        Initialise un document.

        Args:
            iddoc: Identifiant unique
            titre: Titre du document
        """
        self.id = iddoc
        self.titre = titre
        self.disponible = True
        self.dateajout = datetime.now()

    @abstractmethod
    def dureeemprunt(self) -> int:
        """Retourne la durée d'emprunt en jours."""
        pass

    @abstractmethod
    def typedocument(self) -> TypeDocument:
        """Retourne le type du document."""
        pass

    def emprunter(self) -> bool:
        """
        Marque le document comme emprunté.

        Returns:
            True si l'emprunt est possible
        """
        if not self.disponible:
            return False
        self.disponible = False
        return True

    def retourner(self) -> bool:
        """
        Marque le document comme retourné.

        Returns:
            True si le retour est effectué
        """
        if self.disponible:
            return False
        self.disponible = True
        return True

    def str(self) -> str:
        """Représentation string du document."""
        statut = "Disponible" if self.disponible else "Emprunté"
        return f"[{self.id}] {self.titre} - {statut}"

    def eq(self, other: object) -> bool:
        """Comparaison d'égalité."""
        if not isinstance(other, Document):
            return NotImplemented
        return self.id == other.id

    def hash(self) -> int:
        """Hash pour utilisation dans sets et dict."""
        return hash(self.id)

class Livre(Document):
    """
    Livre de bibliothèque.

    Attributes:
        auteur: Auteur du livre
        isbn: Code ISBN
        nombrepages: Nombre de pages
    """

    def init(
        self,
        iddoc: str,
        titre: str,
        auteur: str,
        isbn: str,
        nombrepages: int
    ) -> None:
        """
        Initialise un livre.

        Args:
            iddoc: Identifiant unique
            titre: Titre du livre
            auteur: Auteur
            isbn: Code ISBN
            nombrepages: Nombre de pages
        """
        super().init(iddoc, titre)
        self.auteur = auteur
        self.isbn = isbn
        self.nombrepages = nombrepages

    def dureeemprunt(self) -> int:
        """Durée d'emprunt d'un livre: 21 jours."""
        return 21

    def typedocument(self) -> TypeDocument:
        """Type: livre."""
        return TypeDocument.LIVRE

    def str(self) -> str:
        """Représentation string du livre."""
        base = super().str()
        return f"{base} par {self.auteur}"

class Magazine(Document):
    """
    Magazine de bibliothèque.

    Attributes:
        numero: Numéro du magazine
        mois: Mois de publication
        annee: Année de publication
    """

    def init(
        self,
        iddoc: str,
        titre: str,
        numero: int,
        mois: str,
        annee: int
    ) -> None:
        """
        Initialise un magazine.

        Args:
            iddoc: Identifiant unique
            titre: Titre du magazine
            numero: Numéro
            mois: Mois de publication
            annee: Année de publication
        """
        super().init(iddoc, titre)
        self.numero = numero
        self.mois = mois
        self.annee = annee

    def dureeemprunt(self) -> int:
        """Durée d'emprunt d'un magazine: 7 jours."""
        return 7

    def typedocument(self) -> TypeDocument:
        """Type: magazine."""
        return TypeDocument.MAGAZINE

    def str(self) -> str:
        """Représentation string du magazine."""
        base = super().str()
        return f"{base} - N°{self.numero} ({self.mois} {self.annee})"

class DVD(Document):
    """
    DVD de bibliothèque.

    Attributes:
        realisateur: Réalisateur du film
        dureeminutes: Durée en minutes
    """

    def init(
        self,
        iddoc: str,
        titre: str,
        realisateur: str,
        dureeminutes: int
    ) -> None:
        """
        Initialise un DVD.

        Args:
            iddoc: Identifiant unique
            titre: Titre du film
            realisateur: Réalisateur
            dureeminutes: Durée
        """
        super().init(iddoc, titre)
        self.realisateur = realisateur
        self.dureeminutes = dureeminutes

    def dureeemprunt(self) -> int:
        """Durée d'emprunt d'un DVD: 14 jours."""
        return 14

    def typedocument(self) -> TypeDocument:
        """Type: DVD."""
        return TypeDocument.DVD

    def str(self) -> str:
        """Représentation string du DVD."""
        base = super().str()
        return f"{base} de {self.realisateur}"

@dataclass
class Emprunt:
    """
    Représente un emprunt de document.

    Attributes:
        document: Document emprunté
        dateemprunt: Date de l'emprunt
        dateretourprevue: Date de retour prévue
        dateretoureffective: Date de retour effective (None si en cours)
    """
    document: Document
    dateemprunt: datetime = field(defaultfactory=datetime.now)
    dateretourprevue: datetime = field(init=False)
    dateretoureffective: Optional[datetime] = None

    def postinit(self) -> None:
        """Calcule la date de retour prévue."""
        self.dateretourprevue = (
            self.dateemprunt + timedelta(days=self.document.dureeemprunt())
        )

    @property
    def statut(self) -> StatutEmprunt:
        """Détermine le statut de l'emprunt."""
        if self.dateretoureffective:
            return StatutEmprunt.RETOURNE

        if datetime.now() > self.dateretourprevue:
            return StatutEmprunt.ENRETARD

        return StatutEmprunt.ENCOURS

    @property
    def joursretard(self) -> int:
        """Calcule le nombre de jours de retard."""
        if self.statut != StatutEmprunt.ENRETARD:
            return 0

        datereference = self.dateretoureffective or datetime.now()
        delta = datereference - self.dateretourprevue
        return max(0, delta.days)

    def calculerpenalite(self, tarifjour: float = 0.50) -> float:
        """
        Calcule la pénalité de retard.

        Args:
            tarifjour: Tarif par jour de retard

        Returns:
            Montant de la pénalité
        """
        return self.joursretard  tarifjour

    def str(self) -> str:
        """Représentation string de l'emprunt."""
        retardinfo = f" - {self.joursretard}j de retard" if self.joursretard > 0 else ""
        return (
            f"{self.document.titre} "
            f"(emprunté le {self.dateemprunt.strftime('%d/%m/%Y')}) "
            f"- {self.statut.value}{retardinfo}"
        )

class Adherent:
    """
    Adhérent de la bibliothèque.

    Attributes:
        id: Identifiant unique
        nom: Nom de famille
        prenom: Prénom
        email: Adresse email
    """

    MAXEMPRUNTS: int = 5  # Nombre max d'emprunts simultanés

    def init(
        self,
        idadherent: str,
        nom: str,
        prenom: str,
        email: str
    ) -> None:
        """
        Initialise un adhérent.

        Args:
            idadherent: Identifiant unique
            nom: Nom de famille
            prenom: Prénom
            email: Adresse email
        """
        self.id = idadherent
        self.nom = nom
        self.prenom = prenom
        self.email = email
        self.dateinscription = datetime.now()
        self.empruntsencours: List[Emprunt] = []
        self.historiqueemprunts: List[Emprunt] = []

    @property
    def nomcomplet(self) -> str:
        """Retourne le nom complet."""
        return f"{self.prenom} {self.nom}"

    def peutemprunter(self) -> bool:
        """Vérifie si l'adhérent peut emprunter."""
        # Ne peut pas emprunter si nombre max atteint
        if len(self.empruntsencours) >= self.MAXEMPRUNTS:
            return False

        # Ne peut pas emprunter s'il a des retards
        for emprunt in self.empruntsencours:
            if emprunt.statut == StatutEmprunt.ENRETARD:
                return False

        return True

    def emprunterdocument(self, document: Document) -> bool:
        """
        Emprunte un document.

        Args:
            document: Document à emprunter

        Returns:
            True si l'emprunt est effectué
        """
        if not self.peutemprunter():
            return False

        if not document.emprunter():
            return False

        emprunt = Emprunt(document)
        self.empruntsencours.append(emprunt)
        return True

    def retournerdocument(self, document: Document) -> bool:
        """
        Retourne un document.

        Args:
            document: Document à retourner

        Returns:
            True si le retour est effectué
        """
        # Trouver l'emprunt correspondant
        emprunttrouve = None
        for emprunt in self.empruntsencours:
            if emprunt.document == document:
                emprunttrouve = emprunt
                break

        if not emprunttrouve:
            return False

        # Marquer comme retourné
        emprunttrouve.dateretoureffective = datetime.now()
        document.retourner()

        # Déplacer dans l'historique
        self.empruntsencours.remove(emprunttrouve)
        self.historiqueemprunts.append(emprunttrouve)

        return True

    def penalitestotales(self) -> float:
        """Calcule le total des pénalités."""
        return sum(
            emprunt.calculerpenalite()
            for emprunt in self.empruntsencours
            if emprunt.statut == StatutEmprunt.ENRETARD
        )

    def str(self) -> str:
        """Représentation string de l'adhérent."""
        return (
            f"{self.nomcomplet} ({self.id}) - "
            f"{len(self.empruntsencours)} emprunt(s) en cours"
        )

class Bibliotheque:
    """
    Gestionnaire principal de la bibliothèque.

    Attributes:
        nom: Nom de la bibliothèque
        documents: Collection de documents
        adherents: Collection d'adhérents
    """

    def init(self, nom: str) -> None:
        """
        Initialise la bibliothèque.

        Args:
            nom: Nom de la bibliothèque
        """
        self.nom = nom
        self.documents: Dict[str, Document] = {}
        self.adherents: Dict[str, Adherent] = {}

    # Gestion des documents
    def ajouterdocument(self, document: Document) -> None:
        """
        Ajoute un document au catalogue.

        Args:
            document: Document à ajouter

        Raises:
            ValueError: Si l'ID existe déjà
        """
        if document.id in self.documents:
            raise ValueError(f"Document {document.id} existe déjà")

        self.documents[document.id] = document
        print(f"✓ Document ajouté: {document}")

    def retirerdocument(self, iddoc: str) -> bool:
        """
        Retire un document du catalogue.

        Args:
            iddoc: ID du document

        Returns:
            True si retiré avec succès
        """
        if iddoc not in self.documents:
            return False

        document = self.documents[iddoc]
        if not document.disponible:
            print(f"✗ Impossible de retirer {document}: document emprunté")
            return False

        del self.documents[iddoc]
        print(f"✓ Document retiré: {document}")
        return True

    def rechercherdocuments(
        self,
        titre: Optional[str] = None,
        typedoc: Optional[TypeDocument] = None,
        disponibleseulement: bool = False
    ) -> List[Document]:
        """
        Recherche des documents.

        Args:
            titre: Recherche dans le titre (partielle)
            typedoc: Filtrer par type
            disponibleseulement: Uniquement les documents disponibles

        Returns:
            Liste des documents correspondants
        """
        resultats = list(self.documents.values())

        if titre:
            resultats = [
                d for d in resultats
                if titre.lower() in d.titre.lower()
            ]

        if typedoc:
            resultats = [
                d for d in resultats
                if d.typedocument() == typedoc
            ]

        if disponibleseulement:
            resultats = [d for d in resultats if d.disponible]

        return resultats

    # Gestion des adhérents
    def inscrireadherent(self, adherent: Adherent) -> None:
        """
        Inscrit un adhérent.

        Args:
            adherent: Adhérent à inscrire

        Raises:
            ValueError: Si l'ID existe déjà
        """
        if adherent.id in self.adherents:
            raise ValueError(f"Adhérent {adherent.id} existe déjà")

        self.adherents[adherent.id] = adherent
        print(f"✓ Adhérent inscrit: {adherent}")

    def desinscrireadherent(self, idadherent: str) -> bool:
        """
        Désinscrit un adhérent.

        Args:
            idadherent: ID de l'adhérent

        Returns:
            True si désinscrit avec succès
        """
        if idadherent not in self.adherents:
            return False

        adherent = self.adherents[idadherent]
        if adherent.empruntsencours:
            print(f"✗ Impossible de désinscrire {adherent}: emprunts en cours")
            return False

        del self.adherents[idadherent]
        print(f"✓ Adhérent désinscrit: {adherent}")
        return True

    # Gestion des emprunts
    def emprunter(self, idadherent: str, iddocument: str) -> bool:
        """
        Effectue un emprunt.

        Args:
            idadherent: ID de l'adhérent
            iddocument: ID du document

        Returns:
            True si l'emprunt est effectué
        """
        if idadherent not in self.adherents:
            print(f"✗ Adhérent {idadherent} inconnu")
            return False

        if iddocument not in self.documents:
            print(f"✗ Document {iddocument} inconnu")
            return False

        adherent = self.adherents[idadherent]
        document = self.documents[iddocument]

        if not adherent.peutemprunter():
            print(f"✗ {adherent.nomcomplet} ne peut pas emprunter")
            return False

        if adherent.emprunterdocument(document):
            print(f"✓ {adherent.nomcomplet} a emprunté: {document.titre}")
            return True

        print(f"✗ Document {document.titre} non disponible")
        return False

    def retourner(self, idadherent: str, iddocument: str) -> bool:
        """
        Effectue un retour.

        Args:
            idadherent: ID de l'adhérent
            iddocument: ID du document

        Returns:
            True si le retour est effectué
        """
        if idadherent not in self.adherents:
            return False

        if iddocument not in self.documents:
            return False

        adherent = self.adherents[idadherent]
        document = self.documents[iddocument]

        if adherent.retournerdocument(document):
            penalite = 0
            for emprunt in adherent.historiqueemprunts:
                if emprunt.document == document:
                    penalite = emprunt.calculerpenalite()
                    break

            msg = f"✓ {adherent.nomcomplet} a retourné: {document.titre}"
            if penalite > 0:
                msg += f" (pénalité: {penalite}€)"
            print(msg)
            return True

        print(f"✗ Erreur lors du retour")
        return False

    # Statistiques et rapports
    def statistiques(self) -> Dict[str, any]:
        """Génère des statistiques sur la bibliothèque."""
        totaldocuments = len(self.documents)
        documentsdisponibles = sum(
            1 for d in self.documents.values() if d.disponible
        )

        empruntsencours = sum(
            len(a.empruntsencours) for a in self.adherents.values()
        )

        empruntsenretard = sum(
            1 for a in self.adherents.values()
            for e in a.empruntsencours
            if e.statut == StatutEmprunt.ENRETARD
        )

        penalitestotales = sum(
            a.penalitestotales() for a in self.adherents.values()
        )

        return {
            "totaldocuments": totaldocuments,
            "documentsdisponibles": documentsdisponibles,
            "documentsempruntes": totaldocuments - documentsdisponibles,
            "totaladherents": len(self.adherents),
            "empruntsencours": empruntsencours,
            "empruntsenretard": empruntsenretard,
            "penalitestotales": penalitestotales
        }

    def afficherstatistiques(self) -> None:
        """Affiche les statistiques."""
        stats = self.statistiques()

        print(f"n=== Statistiques - {self.nom} ===")
        print(f"Documents: {stats['totaldocuments']} "
              f"({stats['documentsdisponibles']} disponibles)")
        print(f"Adhérents: {stats['totaladherents']}")
        print(f"Emprunts en cours: {stats['empruntsencours']}")
        print(f"Emprunts en retard: {stats['empruntsenretard']}")
        print(f"Pénalités totales: {stats['penalitestotales']}€")

    def documentspopulaires(self, limite: int = 5) -> List[tuple]:
        """
        Retourne les documents les plus empruntés.

        Args:
            limite: Nombre de documents à retourner

        Returns:
            Liste de tuples (document, nombreemprunts)
        """
        compteur: Dict[Document, int] = {}

        for adherent in self.adherents.values():
            tousemprunts = (
                adherent.empruntsencours +
                adherent.historiqueemprunts
            )
            for emprunt in tousemprunts:
                doc = emprunt.document
                compteur[doc] = compteur.get(doc, 0) + 1

        # Trier par nombre d'emprunts décroissant
        populaires = sorted(
            compteur.items(),
            key=lambda x: x[1],
            reverse=True
        )

        return populaires[:limite]

def demobibliotheque() -> None:
    """Démonstration du système de bibliothèque."""

    # Création de la bibliothèque
    biblio = Bibliotheque("Bibliothèque Municipale")

    print("=== Ajout de documents ===n")

    # Ajout de livres
    livre1 = Livre(
        "L001",
        "1984",
        "George Orwell",
        "978-0451524935",
        328
    )
    livre2 = Livre(
        "L002",
        "Le Petit Prince",
        "Antoine de Saint-Exupéry",
        "978-0156012195",
        96
    )

    biblio.ajouterdocument(livre1)
    biblio.ajouterdocument(livre2)

    # Ajout de magazines
    mag1 = Magazine("M001", "Science & Vie", 1250, "Janvier", 2024)
    biblio.ajouterdocument(mag1)

    # Ajout de DVDs
    dvd1 = DVD("D001", "Inception", "Christopher Nolan", 148)
    biblio.ajouterdocument(dvd1)

    # Inscription d'adhérents
    print("n=== Inscription d'adhérents ===n")

    adherent1 = Adherent("A001", "Dupont", "Marie", "marie.dupont@example.com")
    adherent2 = Adherent("A002", "Martin", "Pierre", "pierre.martin@example.com")

    biblio.inscrireadherent(adherent1)
    biblio.inscrireadherent(adherent2)

    # Emprunts
    print("n=== Emprunts ===n")

    biblio.emprunter("A001", "L001")
    biblio.emprunter("A001", "M001")
    biblio.emprunter("A002", "D001")

    # Recherche
    print("n=== Recherche de documents ===n")

    livresdispo = biblio.rechercherdocuments(
        typedoc=TypeDocument.LIVRE,
        disponibleseulement=True
    )
    print(f"Livres disponibles: {len(livresdispo)}")
    for livre in livresdispo:
        print(f"  - {livre}")

    # Statistiques
    biblio.afficherstatistiques()

    # Retour
    print("n=== Retours ===n")

    biblio.retourner("A001", "L001")

    # Statistiques finales
    biblio.afficherstatistiques()

    # Documents populaires
    print("n=== Documents populaires ===n")
    for doc, nombre in biblio.documentspopulaires(3):
        print(f"{doc.titre}: {nombre} emprunt(s)")

if name == "main":
    demobibliotheque()

Tests unitaires

"""
tests/testlibrary.py - Tests pour le système de bibliothèque
"""

import pytest
from datetime import datetime, timedelta
from project.library import (
    Livre,
    Magazine,
    DVD,
    Adherent,
    Bibliotheque,
    Emprunt,
    StatutEmprunt,
    TypeDocument
)

class TestDocument:
    """Tests pour les classes de documents."""

    def testcreationlivre(self):
        """Test de création d'un livre."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        assert livre.id == "L001"
        assert livre.titre == "Test"
        assert livre.auteur == "Auteur"
        assert livre.disponible is True
        assert livre.dureeemprunt() == 21
        assert livre.typedocument() == TypeDocument.LIVRE

    def testcreationmagazine(self):
        """Test de création d'un magazine."""
        mag = Magazine("M001", "Test Mag", 1, "Janvier", 2024)

        assert mag.numero == 1
        assert mag.dureeemprunt() == 7
        assert mag.typedocument() == TypeDocument.MAGAZINE

    def testcreationdvd(self):
        """Test de création d'un DVD."""
        dvd = DVD("D001", "Test Film", "Réalisateur", 120)

        assert dvd.realisateur == "Réalisateur"
        assert dvd.dureeemprunt() == 14
        assert dvd.typedocument() == TypeDocument.DVD

    def testempruntdocument(self):
        """Test de l'emprunt d'un document."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        assert livre.emprunter() is True
        assert livre.disponible is False
        assert livre.emprunter() is False  # Déjà emprunté

    def testretourdocument(self):
        """Test du retour d'un document."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)

        livre.emprunter()
        assert livre.retourner() is True
        assert livre.disponible is True

class TestEmprunt:
    """Tests pour la classe Emprunt."""

    def testcreationemprunt(self):
        """Test de création d'un emprunt."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        assert emprunt.document == livre
        assert emprunt.statut == StatutEmprunt.ENCOURS

    def testcalculdateretour(self):
        """Test du calcul de la date de retour."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        expecteddate = datetime.now() + timedelta(days=21)
        assert emprunt.dateretourprevue.date() == expecteddate.date()

    def teststatutenretard(self):
        """Test du statut en retard."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        # Simuler un emprunt en retard
        emprunt.dateemprunt = datetime.now() - timedelta(days=30)
        emprunt.dateretourprevue = datetime.now() - timedelta(days=9)

        assert emprunt.statut == StatutEmprunt.ENRETARD
        assert emprunt.joursretard > 0

    def testcalculpenalite(self):
        """Test du calcul des pénalités."""
        livre = Livre("L001", "Test", "Auteur", "123456", 200)
        emprunt = Emprunt(livre)

        # Simuler 10 jours de retard
        emprunt.dateemprunt = datetime.now() - timedelta(days=31)
        emprunt.dateretourprevue = datetime.now() - timedelta(days=10)

        penalite = emprunt.calculerpenalite(0.50)
        assert penalite == 10  0.50

class TestAdherent:
    """Tests pour la classe Adhérent."""

    @pytest.fixture
    def adherent(self):
        """Fixture pour créer un adhérent."""
        return Adherent("A001", "Dupont", "Marie", "marie@example.com")

    @pytest.fixture
    def livre(self):
        """Fixture pour créer un livre."""
        return Livre("L001", "Test", "Auteur", "123456", 200)

    def testcreationadherent(self, adherent):
        """Test de création d'un adhérent."""
        assert adherent.id == "A001"
        assert adherent.nom == "Dupont"
        assert adherent.nomcomplet == "Marie Dupont"
        assert len(adherent.empruntsencours) == 0

    def testempruntdocument(self, adherent, livre):
        """Test d'emprunt de document."""
        assert adherent.emprunterdocument(livre) is True
        assert len(adherent.empruntsencours) == 1
        assert livre.disponible is False

    def testlimiteemprunts(self, adherent):
        """Test de la limite d'emprunts."""
        # Emprunter 5 livres (limite max)
        for i in range(5):
            livre = Livre(f"L{i}", f"Livre {i}", "Auteur", f"ISBN{i}", 200)
            adherent.emprunterdocument(livre)

        # Le 6ème emprunt doit échouer
        livre6 = Livre("L006", "Livre 6", "Auteur", "ISBN6", 200)
        assert adherent.peutemprunter() is False
        assert adherent.emprunterdocument(livre6) is False

    def testretourdocument(self, adherent, livre):
        """Test de retour de document."""
        adherent.emprunterdocument(livre)
        assert adherent.retournerdocument(livre) is True
        assert len(adherent.empruntsencours) == 0
        assert len(adherent.historiqueemprunts) == 1
        assert livre.disponible is True

class TestBibliotheque:
    """Tests pour la classe Bibliothèque."""

    @pytest.fixture
    def bibliotheque(self):
        """Fixture pour créer une bibliothèque."""
        return Bibliotheque("Test Library")

    @pytest.fixture
    def livre(self):
        """Fixture pour créer un livre."""
        return Livre("L001", "Test", "Auteur", "123456", 200)

    @pytest.fixture
    def adherent(self):
        """Fixture pour créer un adhérent."""
        return Adherent("A001", "Dupont", "Marie", "marie@example.com")

    def testcreationbibliotheque(self, bibliotheque):
        """Test de création de bibliothèque."""
        assert bibliotheque.nom == "Test Library"
        assert len(bibliotheque.documents) == 0
        assert len(bibliotheque.adherents) == 0

    def testajouterdocument(self, bibliotheque, livre):
        """Test d'ajout de document."""
        bibliotheque.ajouterdocument(livre)
        assert len(bibliotheque.documents) == 1
        assert "L001" in bibliotheque.documents

    def testajouterdocumentdoublon(self, bibliotheque, livre):
        """Test d'ajout de document avec ID existant."""
        bibliotheque.ajouterdocument(livre)

        with pytest.raises(ValueError):
            bibliotheque.ajouterdocument(livre)

    def testretirerdocument(self, bibliotheque, livre):
        """Test de retrait de document."""
        bibliotheque.ajouterdocument(livre)
        assert bibliotheque.retirerdocument("L001") is True
        assert len(bibliotheque.documents) == 0

    def testrechercherdocuments(self, bibliotheque):
        """Test de recherche de documents."""
        livre1 = Livre("L001", "Python Programming", "Author1", "123", 200)
        livre2 = Livre("L002", "Java Programming", "Author2", "456", 300)
        mag = Magazine("M001", "Python Magazine", 1, "Jan", 2024)

        bibliotheque.ajouterdocument(livre1)
        bibliotheque.ajouterdocument(livre2)
        bibliotheque.ajouterdocument(mag)

        # Recherche par titre
        resultats = bibliotheque.rechercherdocuments(titre="Python")
        assert len(resultats) == 2

        # Recherche par type
        livres = bibliotheque.rechercherdocuments(typedoc=TypeDocument.LIVRE)
        assert len(livres) == 2

    def testinscrireadherent(self, bibliotheque, adherent):
        """Test d'inscription d'adhérent."""
        bibliotheque.inscrireadherent(adherent)
        assert len(bibliotheque.adherents) == 1
        assert "A001" in bibliotheque.adherents

    def testempruntcomplet(self, bibliotheque, livre, adherent):
        """Test du processus d'emprunt complet."""
        bibliotheque.ajouterdocument(livre)
        bibliotheque.inscrireadherent(adherent)

        assert bibliotheque.emprunter("A001", "L001") is True
        assert livre.disponible is False
        assert len(adherent.empruntsencours) == 1

    def testretourcomplet(self, bibliotheque, livre, adherent):
        """Test du processus de retour complet."""
        bibliotheque.ajouterdocument(livre)
        bibliotheque.inscrireadherent(adherent)
        bibliotheque.emprunter("A001", "L001")

        assert bibliotheque.retourner("A001", "L001") is True
        assert livre.disponible is True
        assert len(adherent.empruntsencours) == 0

    def teststatistiques(self, bibliotheque, livre, adherent):
        """Test des statistiques."""
        bibliotheque.ajouterdocument(livre)
        bibliotheque.inscrireadherent(adherent)
        bibliotheque.emprunter("A001", "L001")

        stats = bibliotheque.statistiques()

        assert stats["totaldocuments"] == 1
        assert stats["documentsdisponibles"] == 0
        assert stats["documentsempruntes"] == 1
        assert stats["totaladherents"] == 1
        assert stats["empruntsen_cours"] == 1

if name == "main":
    pytest.main([file, "-v", "--cov=project.library"])

Conclusion

Dans cet article, vous avez découvert la programmation orientée objet en Python avec:

  • Classes de base: Constructeurs, méthodes d’instance, de classe et statiques
  • Encapsulation: Properties, attributs privés, getters/setters
  • Héritage: Classes abstraites, héritage simple et multiple
  • Polymorphisme: Méthodes abstraites, surcharge, duck typing
  • Composition: Préférer la composition à l’héritage
  • Design patterns: Singleton, Factory, Observer, Decorator, Strategy
  • Projet complet: Système de bibliothèque avec tests
  • Bonnes pratiques POO

  • Respecter SOLID:
  • – Single Responsibility: Une classe = une responsabilité
    – Open/Closed: Ouvert à l’extension, fermé à la modification
    – Liskov Substitution: Les sous-classes doivent être substituables
    – Interface Segregation: Interfaces spécifiques plutôt que génériques
    – Dependency Inversion: Dépendre d’abstractions, pas d’implémentations

  • Préférer la composition à l’héritage quand c’est possible
  • Utiliser les dataclasses pour réduire le boilerplate
  • Documenter avec docstrings (PEP 257)
  • Typer avec type hints (PEP 484)
  • Tester chaque classe avec pytest
  • Prochaines étapes

    Continuez votre apprentissage avec:

  • FastAPI pour créer des APIs REST
  • Django pour des applications web complètes
  • SQLAlchemy pour la gestion de bases de données
  • asyncio pour la programmation asynchrone
  • Pandas pour le Data Science
  • Ressources

  • Python Documentation: https://docs.python.org/3/tutorial/classes.html
  • Real Python OOP: https://realpython.com/python3-object-oriented-programming/
  • Design Patterns in Python: https://refactoring.guru/design-patterns/python

Une remarque, un retour ?

Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.