Paradigmes de programmation
💻 Paradigmes de programmation
Approches et philosophies de développement logiciel
I. Introduction aux paradigmes
🎯 Qu'est-ce qu'un paradigme de programmation ?
Un paradigme de programmation est une approche fondamentale pour concevoir et structurer des programmes. Il définit la manière de penser et d'organiser le code, influençant la résolution de problèmes et l'architecture logicielle.
Classification des paradigmes
Paradigmes principaux : - Impératif : Instructions séquentielles (comment faire) - Déclaratif : Description du résultat souhaité (quoi faire) - Orienté objet : Modélisation par objets et classes - Fonctionnel : Calculs par fonctions mathématiques - Logique : Raisonnement par règles et faits
Remarque importante :
La plupart des langages modernes sont multi-paradigmes, permettant de combiner plusieurs approches selon les besoins du projet.
II. Programmation impérative
🔄 Paradigme impératif
Principe : Le programme est une séquence d'instructions qui modifient l'état du système.
Caractéristiques : - Variables mutables - Affectations successives - Structures de contrôle (boucles, conditions) - Procédures et fonctions
Programmation procédurale
Exemple en Python - Style procédural
def calculer_moyenne(notes): """Calcule la moyenne d'une liste de notes""" total = 0 for note in notes: total += note return total / len(notes)
def afficher_resultats(etudiants): """Affiche les résultats des étudiants""" for nom, notes in etudiants.items(): moyenne = calculer_moyenne(notes) print(f"{nom}: {moyenne:.2f}")
if moyenne >= 10:
print(" → Admis")
else:
print(" → Recalé")
Utilisation
etudiants = { "Alice": [15, 12, 18, 14], "Bob": [8, 9, 7, 11], "Charlie": [16, 17, 15, 18] }
afficher_resultats(etudiants)
Avantages et inconvénients
✅ Avantages
Proche du fonctionnement machine Contrôle précis de l'exécution Performance optimisable Facilité de débogage Apprentissage intuitif
❌ Inconvénients
Code difficile à maintenir Réutilisabilité limitée Gestion complexe de l'état Effets de bord nombreux Difficulté de parallélisation
III. Programmation orientée objet (POO)
🏗️ Paradigme orienté objet
Principe : Modélisation du monde réel par des objets qui encapsulent données et comportements.
Concepts fondamentaux : - Encapsulation : Regroupement données/méthodes - Héritage : Réutilisation et spécialisation - Polymorphisme : Même interface, comportements différents - Abstraction : Simplification de la complexité
Exemple complet en Python
Classe de base
class Vehicule: def init(self, marque, modele, annee): self._marque = marque # Attribut protégé self._modele = modele self._annee = annee self._vitesse = 0
@property
def marque(self):
return self._marque
def accelerer(self, increment):
self._vitesse += increment
print(f"Vitesse: {self._vitesse} km/h")
def freiner(self, decrement):
self._vitesse = max(0, self._vitesse - decrement)
print(f"Vitesse: {self._vitesse} km/h")
def __str__(self):
return f"{self._marque} {self._modele} ({self._annee})"
Héritage
class Voiture(Vehicule): def init(self, marque, modele, annee, nb_portes): super().init(marque, modele, annee) self._nb_portes = nb_portes
def klaxonner(self):
print("Bip bip!")
def __str__(self):
return f"{super().__str__()} - {self._nb_portes} portes"
class Moto(Vehicule): def init(self, marque, modele, annee, cylindree): super().init(marque, modele, annee) self._cylindree = cylindree
def faire_wheeling(self):
if self._vitesse > 30:
print("Wheeling!")
else:
print("Trop lent pour un wheeling")
def __str__(self):
return f"{super().__str__()} - {self._cylindree}cc"
Polymorphisme
def presenter_vehicule(vehicule): print(f"Véhicule: {vehicule}") vehicule.accelerer(50)
# Comportement spécifique selon le type
if isinstance(vehicule, Voiture):
vehicule.klaxonner()
elif isinstance(vehicule, Moto):
vehicule.faire_wheeling()
Utilisation
voiture = Voiture("Toyota", "Corolla", 2020, 4) moto = Moto("Honda", "CBR", 2021, 600)
for vehicule in [voiture, moto]: presenter_vehicule(vehicule) print()
Patterns de conception (Design Patterns)
1. Singleton
class DatabaseConnection: _instance = None _connection = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def connect(self):
if self._connection is None:
self._connection = "Connected to database"
print("Nouvelle connexion créée")
return self._connection
def disconnect(self):
if self._connection:
self._connection = None
print("Connexion fermée")
Test
db1 = DatabaseConnection() db2 = DatabaseConnection() print(db1 is db2) # True - même instance
2. Factory
from abc import ABC, abstractmethod
class Animal(ABC): @abstractmethod def faire_bruit(self): pass
class Chien(Animal): def faire_bruit(self): return "Woof!"
class Chat(Animal): def faire_bruit(self): return "Miaou!"
class Vache(Animal): def faire_bruit(self): return "Meuh!"
class AnimalFactory: @staticmethod def creer_animal(type_animal): animals = { "chien": Chien, "chat": Chat, "vache": Vache }
animal_class = animals.get(type_animal.lower())
if animal_class:
return animal_class()
else:
raise ValueError(f"Animal '{type_animal}' non supporté")
Utilisation
for animal_type in ["chien", "chat", "vache"]: animal = AnimalFactory.creer_animal(animal_type) print(f"{animal_type}: {animal.faire_bruit()}")
3. Observer
class Observable: def init(self): self._observers = []
def ajouter_observer(self, observer):
self._observers.append(observer)
def retirer_observer(self, observer):
self._observers.remove(observer)
def notifier_observers(self, *args, **kwargs):
for observer in self._observers:
observer.update(self, *args, **kwargs)
class Thermometre(Observable): def init(self): super().init() self._temperature = 20
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, value):
self._temperature = value
self.notifier_observers(value)
class AffichageTemperature: def init(self, nom): self.nom = nom
def update(self, observable, temperature):
print(f"{self.nom}: Température = {temperature}°C")
class AlerteTemperature: def update(self, observable, temperature): if temperature > 30: print("🔥 ALERTE: Température élevée!") elif temperature
IV. Programmation fonctionnelle
🔢 Paradigme fonctionnel
Principe : Les programmes sont des compositions de fonctions mathématiques pures, sans effets de bord.
Caractéristiques : - Fonctions pures (même entrée → même sortie) - Immutabilité des données - Fonctions de première classe - Récursion privilégiée - Composition de fonctions
Concepts fondamentaux
1. Fonctions pures
Fonction impure (avec effet de bord)
counter = 0
def increment_impure(): global counter counter += 1 return counter
Fonction pure
def increment_pure(value): return value + 1
Fonction pure pour calculer la factorielle
def factorielle(n): if n
2. Fonctions d'ordre supérieur
map() - Applique une fonction à chaque élément
nombres = [1, 2, 3, 4, 5] carres = list(map(lambda x: x**2, nombres)) print(f"Carrés: {carres}") # [1, 4, 9, 16, 25]
filter() - Filtre selon un prédicat
pairs = list(filter(lambda x: x % 2 == 0, nombres)) print(f"Pairs: {pairs}") # [2, 4]
reduce() - Réduit à une seule valeur
from functools import reduce somme = reduce(lambda x, y: x + y, nombres) print(f"Somme: {somme}") # 15
Composition de fonctions
def composer(f, g): return lambda x: f(g(x))
def doubler(x): return x * 2
def ajouter_un(x): return x + 1
Compose: doubler puis ajouter un
composee = composer(ajouter_un, doubler) print(composee(5)) # (5*2)+1 = 11
3. Immutabilité
Structures immutables avec namedtuple
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
def deplacer_point(point, dx, dy): """Retourne un nouveau point déplacé""" return Point(point.x + dx, point.y + dy)
def distance_origine(point): """Calcule la distance à l'origine""" return (point.x2 + point.y2)**0.5
Utilisation
p1 = Point(3, 4) p2 = deplacer_point(p1, 1, 1)
print(f"P1: {p1}, distance: {distance_origine(p1):.2f}") print(f"P2: {p2}, distance: {distance_origine(p2):.2f}")
Liste immutable avec tuple
def ajouter_element(liste, element): """Retourne une nouvelle liste avec l'élément ajouté""" return liste + (element,)
liste1 = (1, 2, 3) liste2 = ajouter_element(liste1, 4) print(f"Liste1: {liste1}") # (1, 2, 3) print(f"Liste2: {liste2}") # (1, 2, 3, 4)
4. Curryfication
Curryfication manuelle
def multiplier_curry(x): def multiplier_par_x(y): return x * y return multiplier_par_x
Utilisation
doubler = multiplier_curry(2) tripler = multiplier_curry(3)
print(doubler(5)) # 10 print(tripler(4)) # 12
Curryfication avec functools.partial
from functools import partial
def puissance(base, exposant): return base ** exposant
Création de fonctions spécialisées
carre = partial(puissance, exposant=2) cube = partial(puissance, exposant=3)
print(carre(5)) # 25 print(cube(3)) # 27
Exemple pratique : Traitement de données
Données d'exemple
etudiants = [ {"nom": "Alice", "age": 20, "notes": [15, 12, 18, 14]}, {"nom": "Bob", "age": 19, "notes": [8, 9, 7, 11]}, {"nom": "Charlie", "age": 21, "notes": [16, 17, 15, 18]}, {"nom": "Diana", "age": 20, "notes": [13, 14, 12, 15]} ]
Style fonctionnel
def calculer_moyenne(notes): return sum(notes) / len(notes)
def ajouter_moyenne(etudiant): return {**etudiant, "moyenne": calculer_moyenne(etudiant["notes"])}
def est_admis(etudiant): return etudiant["moyenne"] >= 12
def formater_resultat(etudiant): statut = "Admis" if etudiant["moyenne"] >= 12 else "Recalé" return f"{etudiant['nom']} ({etudiant['age']} ans): {etudiant['moyenne']:.1f} - {statut}"
Pipeline de traitement fonctionnel
resultats = list(map( formater_resultat, filter( est_admis, map(ajouter_moyenne, etudiants) ) ))
print("Étudiants admis:") for resultat in resultats: print(f" {resultat}")
Version avec compréhension de liste (plus pythonique)
resultats_pythonic = [ formater_resultat(etudiant) for etudiant in [ajouter_moyenne(e) for e in etudiants] if est_admis(etudiant) ]
V. Programmation logique
🧠 Paradigme logique
Principe : Programmation basée sur la logique formelle, utilisant des faits, règles et requêtes.
Caractéristiques : - Déclaration de faits et règles - Inférence automatique - Backtracking - Unification
Exemple conceptuel en Python
Simulation simple d'un système logique
class BaseFaits: def init(self): self.faits = set() self.regles = []
def ajouter_fait(self, fait):
self.faits.add(fait)
def ajouter_regle(self, condition, conclusion):
self.regles.append((condition, conclusion))
def inferer(self):
"""Applique les règles pour inférer de nouveaux faits"""
nouveaux_faits = True
while nouveaux_faits:
nouveaux_faits = False
for condition, conclusion in self.regles:
if condition(self.faits) and conclusion not in self.faits:
self.faits.add(conclusion)
nouveaux_faits = True
print(f"Inféré: {conclusion}")
def interroger(self, fait):
return fait in self.faits
Exemple: Relations familiales
base = BaseFaits()
Faits de base
base.ajouter_fait("parent(jean, marie)") base.ajouter_fait("parent(marie, paul)") base.ajouter_fait("parent(marie, sophie)") base.ajouter_fait("parent(paul, lucas)")
Règles
def regle_grand_parent(faits): # Si X est parent de Y et Y est parent de Z, alors X est grand-parent de Z for fait1 in faits: if fait1.startswith("parent("): x, y = fait1[7:-1].split(", ") for fait2 in faits: if fait2.startswith(f"parent({y},"): z = fait2.split(", ")[1][:-1] return True return False
Application des règles (simplifiée)
print("Faits initiaux:") for fait in sorted(base.faits): print(f" {fait}")
Dans un vrai système Prolog, l'inférence serait automatique
print("\nConclusions possibles:") print(" jean est grand-parent de paul") print(" jean est grand-parent de sophie") print(" marie est grand-parent de lucas")
VI. Programmation concurrente et parallèle
⚡ Paradigme concurrent
Principe : Exécution simultanée de plusieurs tâches pour améliorer les performances et la réactivité.
Concepts : - Concurrence : Gestion de plusieurs tâches simultanément - Parallélisme : Exécution réellement simultanée - Synchronisation : Coordination entre tâches - Communication : Échange de données
Threading en Python
import threading import time import queue from concurrent.futures import ThreadPoolExecutor
Exemple 1: Thread simple
def tache_longue(nom, duree): print(f"Début de {nom}") time.sleep(duree) print(f"Fin de {nom}") return f"Résultat de {nom}"
Exécution séquentielle
print("=== Exécution séquentielle ===") start = time.time() tache_longue("Tâche 1", 2) tache_longue("Tâche 2", 2) print(f"Temps total: {time.time() - start:.1f}s\n")
Exécution avec threads
print("=== Exécution avec threads ===") start = time.time()
thread1 = threading.Thread(target=tache_longue, args=("Tâche 1", 2)) thread2 = threading.Thread(target=tache_longue, args=("Tâche 2", 2))
thread1.start() thread2.start()
thread1.join() thread2.join()
print(f"Temps total: {time.time() - start:.1f}s\n")
Synchronisation avec verrous
import threading import time
Problème de concurrence sans synchronisation
compteur = 0 verrou = threading.Lock()
def incrementer_sans_verrou(): global compteur for _ in range(100000): compteur += 1
def incrementer_avec_verrou(): global compteur for _ in range(100000): with verrou: compteur += 1
Test sans synchronisation
compteur = 0 threads = [] for i in range(5): t = threading.Thread(target=incrementer_sans_verrou) threads.append(t) t.start()
for t in threads: t.join()
print(f"Sans verrou: {compteur} (attendu: 500000)")
Test avec synchronisation
compteur = 0 threads = [] for i in range(5): t = threading.Thread(target=incrementer_avec_verrou) threads.append(t) t.start()
for t in threads: t.join()
print(f"Avec verrou: {compteur} (attendu: 500000)")
Programmation asynchrone
import asyncio import aiohttp import time
Fonction asynchrone
async def telecharger_url(session, url): print(f"Début téléchargement: {url}") async with session.get(url) as response: contenu = await response.text() print(f"Fin téléchargement: {url} ({len(contenu)} caractères)") return len(contenu)
async def telecharger_plusieurs_urls(): urls = [ "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/1", ]
start = time.time()
async with aiohttp.ClientSession() as session:
# Exécution concurrente
taches = [telecharger_url(session, url) for url in urls]
resultats = await asyncio.gather(*taches)
print(f"Temps total: {time.time() - start:.1f}s")
print(f"Tailles: {resultats}")
Exécution
asyncio.run(telecharger_plusieurs_urls())
VII. Comparaison des paradigmes
Paradigme
Avantages
Inconvénients
Cas d'usage
Impératif
Simple, performant, contrôle précis
Difficile à maintenir, effets de bord
Systèmes embarqués, calcul intensif
Orienté objet
Modulaire, réutilisable, maintenable
Complexité, overhead
Applications complexes, interfaces
Fonctionnel
Pas d'effets de bord, parallélisable
Courbe d'apprentissage, performance
Traitement de données, calculs
Logique
Expressif, inférence automatique
Performance, domaine spécialisé
IA, systèmes experts
Concurrent
Performance, réactivité
Complexité, bugs difficiles
Serveurs, interfaces utilisateur
VIII. Évolution des langages
1950s - Langages assembleur Instructions machine, programmation de bas niveau
1960s - Langages structurés FORTRAN, COBOL, ALGOL - Structures de contrôle
1970s - Programmation procédurale C, Pascal - Fonctions et procédures
1980s - Orienté objet C++, Smalltalk - Classes et objets
1990s - Langages interprétés Python, JavaScript, Java - Portabilité
2000s - Langages fonctionnels Haskell, Scala - Programmation fonctionnelle
2010s - Langages modernes Rust, Go, Swift - Sécurité et performance
Langages par paradigme
Impératif C, Assembly, FORTRAN Contrôle direct du matériel
Orienté objet Java, C#, C++, Python Modélisation par objets
Fonctionnel Haskell, Lisp, Erlang, F# Fonctions pures et immutabilité
Logique Prolog, Mercury Raisonnement et inférence
Multi-paradigme Python, JavaScript, Scala Flexibilité d'approche
Concurrent Go, Erlang, Rust Parallélisme natif
IX. Choix du paradigme
🎯 Critères de sélection
Le choix d'un paradigme dépend de plusieurs facteurs : - Nature du problème : Calcul, interface, système - Performance requise : Temps réel, batch - Équipe de développement : Expertise, taille - Maintenance : Évolutivité, lisibilité - Écosystème : Bibliothèques, outils
Recommandations pratiques
Conseils pour le choix :
- Commencez simple : Paradigme impératif/procédural
- Évoluez progressivement : Ajoutez OOP si nécessaire
- Explorez le fonctionnel : Pour le traitement de données
- Considérez la concurrence : Pour les performances
- Restez pragmatique : Mélangez les approches
Exercices pratiques
TP 1 : Comparaison de paradigmes
- Implémentez un système de gestion de bibliothèque en style procédural
- Refactorisez en orienté objet
- Créez une version fonctionnelle pour les requêtes
- Comparez lisibilité, maintenabilité et performance
TP 2 : Patterns de conception
- Implémentez le pattern Strategy pour différents algorithmes de tri
- Utilisez Observer pour un système de notifications
- Créez une Factory pour générer différents types de documents
- Testez la flexibilité et l'extensibilité
TP 3 : Programmation concurrente
- Créez un web scraper séquentiel
- Parallélisez avec threading
- Implémentez une version asynchrone
- Mesurez et comparez les performances
📝 Exercices
🔬 TP Comparaison
🏗️ TP Patterns