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."""
EN_COURS = "en cours"
RETOURNE = "retourné"
EN_RETARD = "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, id_doc: str, titre: str) -> None:
"""
Initialise un document.
Args:
id_doc: Identifiant unique
titre: Titre du document
"""
self.id = id_doc
self.titre = titre
self.disponible = True
self.date_ajout = datetime.now()
@abstractmethod
def duree_emprunt(self) -> int:
"""Retourne la durée d'emprunt en jours."""
pass
@abstractmethod
def type_document(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
nombre_pages: Nombre de pages
"""
def __init__(
self,
id_doc: str,
titre: str,
auteur: str,
isbn: str,
nombre_pages: int
) -> None:
"""
Initialise un livre.
Args:
id_doc: Identifiant unique
titre: Titre du livre
auteur: Auteur
isbn: Code ISBN
nombre_pages: Nombre de pages
"""
super().__init__(id_doc, titre)
self.auteur = auteur
self.isbn = isbn
self.nombre_pages = nombre_pages
def duree_emprunt(self) -> int:
"""Durée d'emprunt d'un livre: 21 jours."""
return 21
def type_document(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,
id_doc: str,
titre: str,
numero: int,
mois: str,
annee: int
) -> None:
"""
Initialise un magazine.
Args:
id_doc: Identifiant unique
titre: Titre du magazine
numero: Numéro
mois: Mois de publication
annee: Année de publication
"""
super().__init__(id_doc, titre)
self.numero = numero
self.mois = mois
self.annee = annee
def duree_emprunt(self) -> int:
"""Durée d'emprunt d'un magazine: 7 jours."""
return 7
def type_document(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
duree_minutes: Durée en minutes
"""
def __init__(
self,
id_doc: str,
titre: str,
realisateur: str,
duree_minutes: int
) -> None:
"""
Initialise un DVD.
Args:
id_doc: Identifiant unique
titre: Titre du film
realisateur: Réalisateur
duree_minutes: Durée
"""
super().__init__(id_doc, titre)
self.realisateur = realisateur
self.duree_minutes = duree_minutes
def duree_emprunt(self) -> int:
"""Durée d'emprunt d'un DVD: 14 jours."""
return 14
def type_document(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é
date_emprunt: Date de l'emprunt
date_retour_prevue: Date de retour prévue
date_retour_effective: Date de retour effective (None si en cours)
"""
document: Document
date_emprunt: datetime = field(default_factory=datetime.now)
date_retour_prevue: datetime = field(init=False)
date_retour_effective: Optional[datetime] = None
def __post_init__(self) -> None:
"""Calcule la date de retour prévue."""
self.date_retour_prevue = (
self.date_emprunt + timedelta(days=self.document.duree_emprunt())
)
@property
def statut(self) -> StatutEmprunt:
"""Détermine le statut de l'emprunt."""
if self.date_retour_effective:
return StatutEmprunt.RETOURNE
if datetime.now() > self.date_retour_prevue:
return StatutEmprunt.EN_RETARD
return StatutEmprunt.EN_COURS
@property
def jours_retard(self) -> int:
"""Calcule le nombre de jours de retard."""
if self.statut != StatutEmprunt.EN_RETARD:
return 0
date_reference = self.date_retour_effective or datetime.now()
delta = date_reference - self.date_retour_prevue
return max(0, delta.days)
def calculer_penalite(self, tarif_jour: float = 0.50) -> float:
"""
Calcule la pénalité de retard.
Args:
tarif_jour: Tarif par jour de retard
Returns:
Montant de la pénalité
"""
return self.jours_retard * tarif_jour
def __str__(self) -> str:
"""Représentation string de l'emprunt."""
retard_info = f" - {self.jours_retard}j de retard" if self.jours_retard > 0 else ""
return (
f"{self.document.titre} "
f"(emprunté le {self.date_emprunt.strftime('%d/%m/%Y')}) "
f"- {self.statut.value}{retard_info}"
)
class Adherent:
"""
Adhérent de la bibliothèque.
Attributes:
id: Identifiant unique
nom: Nom de famille
prenom: Prénom
email: Adresse email
"""
MAX_EMPRUNTS: int = 5 # Nombre max d'emprunts simultanés
def __init__(
self,
id_adherent: str,
nom: str,
prenom: str,
email: str
) -> None:
"""
Initialise un adhérent.
Args:
id_adherent: Identifiant unique
nom: Nom de famille
prenom: Prénom
email: Adresse email
"""
self.id = id_adherent
self.nom = nom
self.prenom = prenom
self.email = email
self.date_inscription = datetime.now()
self.emprunts_en_cours: List[Emprunt] = []
self.historique_emprunts: List[Emprunt] = []
@property
def nom_complet(self) -> str:
"""Retourne le nom complet."""
return f"{self.prenom} {self.nom}"
def peut_emprunter(self) -> bool:
"""Vérifie si l'adhérent peut emprunter."""
# Ne peut pas emprunter si nombre max atteint
if len(self.emprunts_en_cours) >= self.MAX_EMPRUNTS:
return False
# Ne peut pas emprunter s'il a des retards
for emprunt in self.emprunts_en_cours:
if emprunt.statut == StatutEmprunt.EN_RETARD:
return False
return True
def emprunter_document(self, document: Document) -> bool:
"""
Emprunte un document.
Args:
document: Document à emprunter
Returns:
True si l'emprunt est effectué
"""
if not self.peut_emprunter():
return False
if not document.emprunter():
return False
emprunt = Emprunt(document)
self.emprunts_en_cours.append(emprunt)
return True
def retourner_document(self, document: Document) -> bool:
"""
Retourne un document.
Args:
document: Document à retourner
Returns:
True si le retour est effectué
"""
# Trouver l'emprunt correspondant
emprunt_trouve = None
for emprunt in self.emprunts_en_cours:
if emprunt.document == document:
emprunt_trouve = emprunt
break
if not emprunt_trouve:
return False
# Marquer comme retourné
emprunt_trouve.date_retour_effective = datetime.now()
document.retourner()
# Déplacer dans l'historique
self.emprunts_en_cours.remove(emprunt_trouve)
self.historique_emprunts.append(emprunt_trouve)
return True
def penalites_totales(self) -> float:
"""Calcule le total des pénalités."""
return sum(
emprunt.calculer_penalite()
for emprunt in self.emprunts_en_cours
if emprunt.statut == StatutEmprunt.EN_RETARD
)
def __str__(self) -> str:
"""Représentation string de l'adhérent."""
return (
f"{self.nom_complet} ({self.id}) - "
f"{len(self.emprunts_en_cours)} 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 ajouter_document(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 retirer_document(self, id_doc: str) -> bool:
"""
Retire un document du catalogue.
Args:
id_doc: ID du document
Returns:
True si retiré avec succès
"""
if id_doc not in self.documents:
return False
document = self.documents[id_doc]
if not document.disponible:
print(f"✗ Impossible de retirer {document}: document emprunté")
return False
del self.documents[id_doc]
print(f"✓ Document retiré: {document}")
return True
def rechercher_documents(
self,
titre: Optional[str] = None,
type_doc: Optional[TypeDocument] = None,
disponible_seulement: bool = False
) -> List[Document]:
"""
Recherche des documents.
Args:
titre: Recherche dans le titre (partielle)
type_doc: Filtrer par type
disponible_seulement: 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 type_doc:
resultats = [
d for d in resultats
if d.type_document() == type_doc
]
if disponible_seulement:
resultats = [d for d in resultats if d.disponible]
return resultats
# Gestion des adhérents
def inscrire_adherent(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 desinscrire_adherent(self, id_adherent: str) -> bool:
"""
Désinscrit un adhérent.
Args:
id_adherent: ID de l'adhérent
Returns:
True si désinscrit avec succès
"""
if id_adherent not in self.adherents:
return False
adherent = self.adherents[id_adherent]
if adherent.emprunts_en_cours:
print(f"✗ Impossible de désinscrire {adherent}: emprunts en cours")
return False
del self.adherents[id_adherent]
print(f"✓ Adhérent désinscrit: {adherent}")
return True
# Gestion des emprunts
def emprunter(self, id_adherent: str, id_document: str) -> bool:
"""
Effectue un emprunt.
Args:
id_adherent: ID de l'adhérent
id_document: ID du document
Returns:
True si l'emprunt est effectué
"""
if id_adherent not in self.adherents:
print(f"✗ Adhérent {id_adherent} inconnu")
return False
if id_document not in self.documents:
print(f"✗ Document {id_document} inconnu")
return False
adherent = self.adherents[id_adherent]
document = self.documents[id_document]
if not adherent.peut_emprunter():
print(f"✗ {adherent.nom_complet} ne peut pas emprunter")
return False
if adherent.emprunter_document(document):
print(f"✓ {adherent.nom_complet} a emprunté: {document.titre}")
return True
print(f"✗ Document {document.titre} non disponible")
return False
def retourner(self, id_adherent: str, id_document: str) -> bool:
"""
Effectue un retour.
Args:
id_adherent: ID de l'adhérent
id_document: ID du document
Returns:
True si le retour est effectué
"""
if id_adherent not in self.adherents:
return False
if id_document not in self.documents:
return False
adherent = self.adherents[id_adherent]
document = self.documents[id_document]
if adherent.retourner_document(document):
penalite = 0
for emprunt in adherent.historique_emprunts:
if emprunt.document == document:
penalite = emprunt.calculer_penalite()
break
msg = f"✓ {adherent.nom_complet} 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."""
total_documents = len(self.documents)
documents_disponibles = sum(
1 for d in self.documents.values() if d.disponible
)
emprunts_en_cours = sum(
len(a.emprunts_en_cours) for a in self.adherents.values()
)
emprunts_en_retard = sum(
1 for a in self.adherents.values()
for e in a.emprunts_en_cours
if e.statut == StatutEmprunt.EN_RETARD
)
penalites_totales = sum(
a.penalites_totales() for a in self.adherents.values()
)
return {
"total_documents": total_documents,
"documents_disponibles": documents_disponibles,
"documents_empruntes": total_documents - documents_disponibles,
"total_adherents": len(self.adherents),
"emprunts_en_cours": emprunts_en_cours,
"emprunts_en_retard": emprunts_en_retard,
"penalites_totales": penalites_totales
}
def afficher_statistiques(self) -> None:
"""Affiche les statistiques."""
stats = self.statistiques()
print(f"n=== Statistiques - {self.nom} ===")
print(f"Documents: {stats['total_documents']} "
f"({stats['documents_disponibles']} disponibles)")
print(f"Adhérents: {stats['total_adherents']}")
print(f"Emprunts en cours: {stats['emprunts_en_cours']}")
print(f"Emprunts en retard: {stats['emprunts_en_retard']}")
print(f"Pénalités totales: {stats['penalites_totales']}€")
def documents_populaires(self, limite: int = 5) -> List[tuple]:
"""
Retourne les documents les plus empruntés.
Args:
limite: Nombre de documents à retourner
Returns:
Liste de tuples (document, nombre_emprunts)
"""
compteur: Dict[Document, int] = {}
for adherent in self.adherents.values():
tous_emprunts = (
adherent.emprunts_en_cours +
adherent.historique_emprunts
)
for emprunt in tous_emprunts:
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 demo_bibliotheque() -> 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.ajouter_document(livre1)
biblio.ajouter_document(livre2)
# Ajout de magazines
mag1 = Magazine("M001", "Science & Vie", 1250, "Janvier", 2024)
biblio.ajouter_document(mag1)
# Ajout de DVDs
dvd1 = DVD("D001", "Inception", "Christopher Nolan", 148)
biblio.ajouter_document(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.inscrire_adherent(adherent1)
biblio.inscrire_adherent(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")
livres_dispo = biblio.rechercher_documents(
type_doc=TypeDocument.LIVRE,
disponible_seulement=True
)
print(f"Livres disponibles: {len(livres_dispo)}")
for livre in livres_dispo:
print(f" - {livre}")
# Statistiques
biblio.afficher_statistiques()
# Retour
print("n=== Retours ===n")
biblio.retourner("A001", "L001")
# Statistiques finales
biblio.afficher_statistiques()
# Documents populaires
print("n=== Documents populaires ===n")
for doc, nombre in biblio.documents_populaires(3):
print(f"{doc.titre}: {nombre} emprunt(s)")
if __name__ == "__main__":
demo_bibliotheque()
Tests unitaires
"""
tests/test_library.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 test_creation_livre(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.duree_emprunt() == 21
assert livre.type_document() == TypeDocument.LIVRE
def test_creation_magazine(self):
"""Test de création d'un magazine."""
mag = Magazine("M001", "Test Mag", 1, "Janvier", 2024)
assert mag.numero == 1
assert mag.duree_emprunt() == 7
assert mag.type_document() == TypeDocument.MAGAZINE
def test_creation_dvd(self):
"""Test de création d'un DVD."""
dvd = DVD("D001", "Test Film", "Réalisateur", 120)
assert dvd.realisateur == "Réalisateur"
assert dvd.duree_emprunt() == 14
assert dvd.type_document() == TypeDocument.DVD
def test_emprunt_document(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 test_retour_document(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 test_creation_emprunt(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.EN_COURS
def test_calcul_date_retour(self):
"""Test du calcul de la date de retour."""
livre = Livre("L001", "Test", "Auteur", "123456", 200)
emprunt = Emprunt(livre)
expected_date = datetime.now() + timedelta(days=21)
assert emprunt.date_retour_prevue.date() == expected_date.date()
def test_statut_en_retard(self):
"""Test du statut en retard."""
livre = Livre("L001", "Test", "Auteur", "123456", 200)
emprunt = Emprunt(livre)
# Simuler un emprunt en retard
emprunt.date_emprunt = datetime.now() - timedelta(days=30)
emprunt.date_retour_prevue = datetime.now() - timedelta(days=9)
assert emprunt.statut == StatutEmprunt.EN_RETARD
assert emprunt.jours_retard > 0
def test_calcul_penalite(self):
"""Test du calcul des pénalités."""
livre = Livre("L001", "Test", "Auteur", "123456", 200)
emprunt = Emprunt(livre)
# Simuler 10 jours de retard
emprunt.date_emprunt = datetime.now() - timedelta(days=31)
emprunt.date_retour_prevue = datetime.now() - timedelta(days=10)
penalite = emprunt.calculer_penalite(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 test_creation_adherent(self, adherent):
"""Test de création d'un adhérent."""
assert adherent.id == "A001"
assert adherent.nom == "Dupont"
assert adherent.nom_complet == "Marie Dupont"
assert len(adherent.emprunts_en_cours) == 0
def test_emprunt_document(self, adherent, livre):
"""Test d'emprunt de document."""
assert adherent.emprunter_document(livre) is True
assert len(adherent.emprunts_en_cours) == 1
assert livre.disponible is False
def test_limite_emprunts(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.emprunter_document(livre)
# Le 6ème emprunt doit échouer
livre6 = Livre("L006", "Livre 6", "Auteur", "ISBN6", 200)
assert adherent.peut_emprunter() is False
assert adherent.emprunter_document(livre6) is False
def test_retour_document(self, adherent, livre):
"""Test de retour de document."""
adherent.emprunter_document(livre)
assert adherent.retourner_document(livre) is True
assert len(adherent.emprunts_en_cours) == 0
assert len(adherent.historique_emprunts) == 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 test_creation_bibliotheque(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 test_ajouter_document(self, bibliotheque, livre):
"""Test d'ajout de document."""
bibliotheque.ajouter_document(livre)
assert len(bibliotheque.documents) == 1
assert "L001" in bibliotheque.documents
def test_ajouter_document_doublon(self, bibliotheque, livre):
"""Test d'ajout de document avec ID existant."""
bibliotheque.ajouter_document(livre)
with pytest.raises(ValueError):
bibliotheque.ajouter_document(livre)
def test_retirer_document(self, bibliotheque, livre):
"""Test de retrait de document."""
bibliotheque.ajouter_document(livre)
assert bibliotheque.retirer_document("L001") is True
assert len(bibliotheque.documents) == 0
def test_rechercher_documents(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.ajouter_document(livre1)
bibliotheque.ajouter_document(livre2)
bibliotheque.ajouter_document(mag)
# Recherche par titre
resultats = bibliotheque.rechercher_documents(titre="Python")
assert len(resultats) == 2
# Recherche par type
livres = bibliotheque.rechercher_documents(type_doc=TypeDocument.LIVRE)
assert len(livres) == 2
def test_inscrire_adherent(self, bibliotheque, adherent):
"""Test d'inscription d'adhérent."""
bibliotheque.inscrire_adherent(adherent)
assert len(bibliotheque.adherents) == 1
assert "A001" in bibliotheque.adherents
def test_emprunt_complet(self, bibliotheque, livre, adherent):
"""Test du processus d'emprunt complet."""
bibliotheque.ajouter_document(livre)
bibliotheque.inscrire_adherent(adherent)
assert bibliotheque.emprunter("A001", "L001") is True
assert livre.disponible is False
assert len(adherent.emprunts_en_cours) == 1
def test_retour_complet(self, bibliotheque, livre, adherent):
"""Test du processus de retour complet."""
bibliotheque.ajouter_document(livre)
bibliotheque.inscrire_adherent(adherent)
bibliotheque.emprunter("A001", "L001")
assert bibliotheque.retourner("A001", "L001") is True
assert livre.disponible is True
assert len(adherent.emprunts_en_cours) == 0
def test_statistiques(self, bibliotheque, livre, adherent):
"""Test des statistiques."""
bibliotheque.ajouter_document(livre)
bibliotheque.inscrire_adherent(adherent)
bibliotheque.emprunter("A001", "L001")
stats = bibliotheque.statistiques()
assert stats["total_documents"] == 1
assert stats["documents_disponibles"] == 0
assert stats["documents_empruntes"] == 1
assert stats["total_adherents"] == 1
assert stats["emprunts_en_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
- Respecter SOLID:
- 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
- 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
- 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
Bonnes pratiques POO
– 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
Prochaines étapes
Continuez votre apprentissage avec: