Files
TermNSI/Paradigmes/README.md

14 KiB
Raw Blame History

Paradigmes de programmation

Un paradigme est à la programmation ce quun style culinaire est à la cuisine : il impose des règles, des outils, et une manière de faire.

bo


Introduction

En programmation, il nexiste pas quune seule bonne façon de penser. Comme pour résoudre un problème de maths ou construire une maison, plusieurs approches sont possibles. Ces manières de faire sappellent des paradigmes.

On appelle paradigme une méthode de programmation.

On peut regrouper les différents paradigmes en 2 catégories :

  • la programmation impérative ;
  • la programmation déclarative.

Lorsque l'on utilise la programmation impérative, on décrit le comment / la solution. On la retrouve dans plusieurs langages :

  • Structurée (FORTRAN, C)
  • Procédurale (FORTRAN, C)
  • Objet (C++, Java, Python)
  • Modulaire (C++, Java, Python)

Lorsque l'on utilise la programmation déclarative, on décrit le quoi / le problème. On la retrouve dans plusieurs langages :

  • Descriptif (HTML, SQL)
  • Fonctionnel (LISP, CAML)
  • Logique (Prolog, SQL)

Programmation fonctionnelle

Les différences

La programmation impérative, bien que versatile, conduit rapidement à des programmes difficiles à comprendre et à maintenir, où la démonstration de la correction est impossible.

Ceci est dû au fait qu'un traitement de données :

  • dépend de paramètres extérieurs non maîtrisés : variables ;
  • modifie les données, ou dautres : affectation ;
  • réalise une tâche complexe : une fonction complexe.
a = 3

répéter 5 fois :
    a = a + 2

afficher a

La programmation fonctionnelle pure sabsout de ces problèmes :

  • pas de variables
  • pas daffectation
  • enchaînement de fonctions simples.
f : x -> x + 2

afficher f(f(f(f(f(3)))))

Le principe

Le but d'un traitement informatique est de produire des données à partir de données.

Le programme est une une fonction qui associe des données sortantes à des données entrantes.

f : données entrantes -> données sortantes
Pas d'état interne
Les données sont non mutables

Un traitement complexe s'obtiendra en composant plusieurs fonctions intermédiaires simples et une boucle sobtiendra par une fonction qui sappelle elle-même : récursivité.

En clair : La variable n'existe pas !.

Avantages et les inconvénients

Pour les avantages :

  • Absence deffets de bord.
  • Toute fonction fournit un résultat prédictible qui est toujours le même.
  • Clarifie linterprétation du programme et facilite sa correction.
  • Évite lapparition de bugs non prédictibles.

En revanche, on retrouve des inconvénients :

  • Un traitement classique nécessite un grand nombre de fonctions.
  • Nuit au développement de grosses applications.
  • Gourmand en ressources (stockage des résultats intermédiaires, pile des appels récursifs).
  • Certaines fonctions doivent pouvoir modifier des données extérieures, ou en dépendre.

Effets de bord

Une fonction à effet de bord est une fonction qui modifie ou utilise une variable extérieure à son environnement local (variables globales, opérations dentrées/sorties).

À nutiliser que lorsque cela est nécessaire !

Les effets de bord peuvent être évités facilement grâce au principe de localité des variables et la programmation fonctionnelle.

# Fonction avec effets de bord :

def ajoute_k_a_n(k):
    global n
    n = n + k
    return n

n = 2
print("Avec k=2 : ", ajoute_k_a_n(2))
print("Avec k=3 : ", ajoute_k_a_n(3))
# Fonction sans effets de bord :

def ajoute_k_a_n(n, k):
    return n + k

n = 2
print("Avec k=2 : ", ajoute_k_a_n(n, 2))
print("Avec k=3 : ", ajoute_k_a_n(n, 3))

Fonctions lambdas

λ-calcul (années 1930) : un système formel inventé par Alonzo Church qui fonde les concepts de fonction et d'application. Il s'agit du premier formalisme qui définit et caractérise les fonctions récursives.

lambda : x -> expression(x)

Tout comme la machine de Turing, le λ-calcul peut représenter nimporte quel calcul (hypothèse de Church-Turing, chaque approche peut être exprimée dans lautre).

La programmation fonctionnelle puise son origine du λ-calcul alors que la programmation impérative puise son origine de la machine de Turing.

En Python, la fonction lambda se définit à l'aide du mot-clé lambda :

add_a_b = lambda a,b: a + b

entre_10_et_20 = lambda x: 10 < x < 20

Remarque : inutile d'écrire True if condition else False - la condition est déjà un booléen, on la renvoie directement.

Appelée aussi fonction anonyme, il ne faut pas en abuser en Python, elle :

  • est utile pour définir une fonction simple, proche dune définition mathématique, en une ligne ;
  • est utile pour créer la fonction de sortie dune fonction dordre supérieur ;
  • est utile pour composer les fonctions ;
  • peut utiliser les mots clés if et else ;
  • peut sutiliser avec les fonctions dordres supérieur (exemple du tri de Python) ;
  • peut sutiliser conjointement à la compréhension de listes.

Fonctions et listes

Le mapping

Appliquer une fonction à chaque élément d'une liste.

f = lambda x: x*5
liste = [2, 7, -1]
resultat = list(map(f, liste))   # [10, 35, -5]

Remarque : map renvoie un itérateur, pas une liste. Sans le list(), un print(resultat) afficherait <map object at 0x...>.

La réduction

Combiner les éléments dune liste en appliquant une fonction à laide dun accumulateur (à létat initial, acc prend la première valeur de la liste et x la deuxième, le résultat est stocké dans acc).

import functools

liste = [1, 3, 5, 6, 2]
f_somme = lambda acc, x: acc + x
somme = functools.reduce(f_somme, liste)   # 17
liste = [1, 3, 5, 6, 2]
f_maximum = lambda acc, x: acc if acc > x else x
maximum = functools.reduce(f_maximum, liste)

Le filtrage

Sélectionne les éléments d'une liste vérifiant un certain critère.

liste = [1, 3, 5, 6, 2]
f_filtre = lambda x: x >= 3
liste_filtree = list(filter(f_filtre, liste))   # [3, 5, 6]

Boucler en programmation fonctionnelle

On utilise la récursivité, la fonction sappelant elle-même un certain nombre de fois.

def ajoute_5(n, x):
    if n == 0:
        return x
    else:
        return ajoute_5(n - 1, x + 5)

resultat = ajoute_5(3, 2)
print(resultat)

On peut aussi plus simplement appliquer une réduction sur une liste de n éléments.

ajoute_5 = lambda acc, x: acc + 5
resultat = functools.reduce(ajoute_5, range(3), 2) # avec 2 pour valeur initiale de l'accumulateur
print(resultat)

Exemples de langages multi-paradigmes

  • Python : impératif, objet, fonctionnel
  • JavaScript : impératif, objet, fonctionnel
  • Scala : objet + fonctionnel
  • Rust : impératif + fonctionnel

Exercice

Voici une tâche simple : nous voulons faire cuire des pâtes.

Écrire la recette sous forme de code python :

  • Version impérative : liste dinstructions étape par étape.
  • Version fonctionnelle : une fonction pure qui prend un état (pâtes non cuites) et retourne un nouvel état (pâtes cuites).
  • Version orientée objet : créer une classe Pates avec une méthode cuire().

??? success "Correction"

## Version impérative

Liste dinstructions étape par étape, avec modification détat.

```python
def cuire_pates_imperatif():
    """Fait cuire des pâtes de manière impérative."""

    eau = "froide"
    pates = "crues"
    sel = False

    print("Remplissage de la casserole...")
    quantite_eau = 1.5

    print("Chauffage de leau...")
    eau = "chaude"

    print("Attente de lébullition...")
    eau = "bouillante"

    print("Ajout du sel...")
    sel = True

    print("Ajout des pâtes...")
    pates = "en cuisson"

    temps_cuisson = 10
    print(f"Cuisson pendant {temps_cuisson} minutes...")

    print("Égouttage des pâtes...")
    pates = "cuites"

    print(f"Résultat : pâtes {pates}")
    return pates

resultat = cuire_pates_imperatif()
```

**Caractéristiques :**

- Modification détat (variables `eau`, `pates`, `sel`)
- Instructions séquentielles
- Effets de bord (print)

---

## Version fonctionnelle

Une fonction pure qui prend un état et retourne un nouvel état, sans modification.

```python
def remplir_casserole(etat):
    return {**etat, "eau": "froide", "quantite_eau": 1.5}

def chauffer_eau(etat):
    return {**etat, "eau": "chaude"}

def faire_bouillir(etat):
    return {**etat, "eau": "bouillante"}

def saler(etat):
    return {**etat, "sel": True}

def ajouter_pates(etat):
    return {**etat, "pates": "en cuisson"}

def cuire(etat, duree):
    return {**etat, "temps_cuisson": duree, "pates": "cuites"}

def egoutter(etat):
    return {**etat, "egouttees": True}

def cuire_pates_fonctionnel(etat_initial):
    etat = remplir_casserole(etat_initial)
    etat = chauffer_eau(etat)
    etat = faire_bouillir(etat)
    etat = saler(etat)
    etat = ajouter_pates(etat)
    etat = cuire(etat, 10)
    etat = egoutter(etat)
    return etat

etat_initial = {"pates": "crues", "sel": False}
resultat = cuire_pates_fonctionnel(etat_initial)
print(f"État final : {resultat}")
print(f"État initial préservé : {etat_initial}")
```

**Caractéristiques :**

- Pas de modification détat (immutabilité)
- Fonctions pures (même entrée → même sortie)
- Composition de fonctions
- Létat initial nest jamais modifié

---

## Version orientée objet

Une classe `Pates` avec des attributs et une méthode `cuire()`.

```python
class Casserole:
    def __init__(self):
        self.eau = None
        self.temperature = "froide"
        self.sel = False

    def remplir(self, quantite=1.5):
        self.eau = quantite
        print(f"Casserole remplie avec {quantite}L deau")

    def chauffer(self):
        self.temperature = "bouillante"
        print("Leau bout !")

    def saler(self):
        self.sel = True
        print("Sel ajouté")


class Pates:
    def __init__(self, type_pates="spaghetti", quantite=500):
        self.type = type_pates
        self.quantite = quantite
        self.etat = "crues"
        self.temps_cuisson = 0

    def cuire(self, casserole, duree=10):
        if casserole.eau is None:
            raise ValueError("La casserole doit contenir de leau !")
        if casserole.temperature != "bouillante":
            raise ValueError("Leau doit être bouillante !")
        self.etat = "en cuisson"
        self.temps_cuisson = duree
        self.etat = "cuites"
        print(f"Les {self.type} sont cuites !")

    def egoutter(self):
        if self.etat != "cuites":
            raise ValueError("Les pâtes doivent être cuites avant dêtre égouttées !")
        self.etat = "prêtes"

    def __str__(self):
        return f"Pates({self.type}, {self.quantite}g, état={self.etat})"


casserole = Casserole()
pates = Pates("fusilli", 400)
casserole.remplir(2.0)
casserole.chauffer()
casserole.saler()
pates.cuire(casserole, duree=12)
pates.egoutter()
print(f"Fin : {pates}")
```

**Caractéristiques :**

- Encapsulation (attributs et méthodes regroupés)
- Objets avec état interne
- Interactions entre objets (Pates et Casserole)

---

## Tableau comparatif

| Aspect | Impératif | Fonctionnel | Orienté Objet |
|--------|-----------|-------------|---------------|
| **État** | Variables modifiées | États immuables | Attributs dobjets |
| **Données** | Variables simples | Dictionnaires/tuples | Objets |
| **Actions** | Instructions séquentielles | Fonctions composées | Méthodes |
| **Modification** | Sur place | Nouvel état retourné | Via méthodes |
| **Réutilisabilité** | Faible | Moyenne | Forte |
| **Testabilité** | Difficile | Facile | Moyenne |

---

## Bonus : version lambda

```python
import functools

etapes = [
    lambda e: {**e, "eau": 1.5},
    lambda e: {**e, "temperature": "bouillante"},
    lambda e: {**e, "sel": True},
    lambda e: {**e, "pates": "en cuisson"},
    lambda e: {**e, "temps": 10, "pates": "cuites"},
    lambda e: {**e, "egouttees": True}
]

cuire_pates_lambda = lambda etat: functools.reduce(
    lambda e, f: f(e),
    etapes,
    etat
)

resultat = cuire_pates_lambda({"pates": "crues"})
print(resultat)
```

Auteurs : Florian Mathieu, Enzo Frémeaux, Timothée Decoster

Licence CC BY NC

Licence Creative Commons
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas dUtilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International.