Exercices : programmation fonctionnelle

Exercice 1 : QCM

Pour chaque question, une seule réponse est correcte.

1. Parmi les caractéristiques suivantes, laquelle est propre au paradigme fonctionnel ?

  • A. L’utilisation de boucles for et while
  • B. L’encapsulation de données dans des objets
  • C. La composition de fonctions et l’absence d’effets de bord
  • D. La modification de variables globales
Correction

C. Le paradigme fonctionnel repose sur la composition de fonctions pures (sans effets de bord). A et D sont des caractéristiques du paradigme impératif. B est propre au paradigme objet.

2. Quel est le résultat de list(map(lambda x: x ** 2, [1, 2, 3])) ?

  • A. [2, 4, 6]
  • B. [1, 8, 27]
  • C. [1, 2, 3]
  • D. [1, 4, 9]
Correction

D. map applique lambda x: x ** 2 à chaque élément : $1^2 = 1$, $2^2 = 4$, $3^2 = 9$. A correspond à 2 * x. B correspond à x ** 3. C correspond à la fonction identité.

3. Qu’est-ce qu’une fonction d’ordre supérieur ?

  • A. Une fonction qui prend une autre fonction en paramètre ou qui renvoie une fonction
  • B. Une fonction qui s’appelle elle-même
  • C. Une fonction définie à l’intérieur d’une autre fonction
  • D. Une fonction qui modifie une variable globale
Correction

A. Par exemple, map, filter et sorted sont des fonctions d’ordre supérieur car elles prennent une fonction en paramètre. B décrit la récursivité. C décrit une fonction imbriquée (closure). D décrit un effet de bord.

4. Quelle expression Python est écrite en style fonctionnel ?

  • A. result = 0 ; for x in L: result += x
  • B. while i < len(L): i += 1
  • C. sum(filter(lambda x: x > 0, L))
  • D. obj.calculer(L)
Correction

C. Cette expression compose sum, filter et lambda sans modifier de variable. A et B utilisent des boucles et des affectations (impératif). D utilise une méthode sur un objet (paradigme objet).


Exercice 2 : reconnaître le paradigme

Pour chaque extrait de code, indiquez s’il est écrit en style impératif, fonctionnel ou objet. Justifiez.

a)

def somme(L):
    total = 0
    for x in L:
        total += x
    return total

b)

def somme(L):
    return 0 if L == [] else L[0] + somme(L[1:])

c)

class Calcul:
    def __init__(self, L):
        self.L = L
    def somme(self):
        return sum(self.L)

d)

from functools import reduce
somme = lambda L: reduce(lambda acc, x: acc + x, L, 0)
Correction

a) Impératif. Utilisation d’une variable modifiable total, d’une boucle for et d’une affectation +=.

b) Fonctionnel. Pas de variable modifiable, pas de boucle : seulement une expression conditionnelle et un appel récursif.

c) Objet. Les données (L) sont encapsulées dans un attribut de l’objet, et le traitement est une méthode (somme).

d) Fonctionnel. Utilisation de reduce (fonction d’ordre supérieur) et de lambda (fonctions anonymes), sans variable ni boucle.


Exercice 3 : map, filter et lambda (exercice guidé)

a) Complétez les expressions suivantes (notées ...) :

L = [3, 7, 2, 9, 4]

# Doubler chaque élément
list(map(lambda x: ..., L))          # → [6, 14, 4, 18, 8]

# Garder seulement les éléments impairs
list(filter(lambda x: ..., L))       # → [3, 7, 9]

# Convertir en chaînes
list(map(..., L))                     # → ['3', '7', '2', '9', '4']

b) Écrivez les expressions pour obtenir, à partir de L = [3, 7, 2, 9, 4] :

  1. la liste des carrés : [9, 49, 4, 81, 16] ;
  2. la liste des éléments pairs : [2, 4] ;
  3. la liste des éléments supérieurs à 5 : [7, 9].

Donnez chaque réponse avec map/filter + lambda, puis avec une compréhension de liste.

c) À partir de mots = ['Python', 'est', 'un', 'langage'], obtenir la liste des longueurs : [6, 3, 2, 7].

d) À partir de L = [-3, 5, -1, 8, -2, 7], obtenir la liste des valeurs absolues des éléments négatifs : [3, 1, 2] (combinaison de filter et map).

Correction

a) Compléments :

list(map(lambda x: 2 * x, L))          # doubler
list(filter(lambda x: x % 2 != 0, L))  # impairs
list(map(str, L))                        # convertir en chaînes

Pour le dernier, str est déjà une fonction : pas besoin de lambda.

b)

  1. Carrés :
list(map(lambda x: x ** 2, L))    # avec map
[x ** 2 for x in L]               # en compréhension
  1. Éléments pairs :
list(filter(lambda x: x % 2 == 0, L))
[x for x in L if x % 2 == 0]
  1. Éléments supérieurs à 5 :
list(filter(lambda x: x > 5, L))
[x for x in L if x > 5]

c)

list(map(len, mots))
# ou [len(m) for m in mots]

d)

list(map(abs, filter(lambda x: x < 0, L)))
# ou [-x for x in L if x < 0]

On filtre d’abord les négatifs, puis on applique la valeur absolue.


Exercice 4 : sorted avec key et lambda

a) Trier la liste mots = ['Python', 'est', 'un', 'langage', 'multiparadigme'] par longueur croissante.

b) Trier la liste eleves = [('Alice', 15), ('Bob', 12), ('Clara', 18), ('David', 14)] par note décroissante.

c) Trier la liste points = [(1, 5), (3, 2), (0, 0), (4, 1)] par distance croissante à l’origine.

Correction

a)

sorted(mots, key=len)
# ['un', 'est', 'Python', 'langage', 'multiparadigme']

len est directement passée comme fonction clé.

b)

sorted(eleves, key=lambda e: e[1], reverse=True)
# [('Clara', 18), ('Alice', 15), ('David', 14), ('Bob', 12)]

La clé extrait la note (indice 1). reverse=True inverse l’ordre.

c)

from math import sqrt
sorted(points, key=lambda p: sqrt(p[0]**2 + p[1]**2))
# [(0, 0), (3, 2), (4, 1), (1, 5)]

On peut aussi utiliser p[0]**2 + p[1]**2 (le carré suffit pour comparer les distances).


Exercice 5 : réécriture impératif vers fonctionnel

Réécrivez chaque fonction en style fonctionnel (pas de variable modifiable, pas de boucle, récursivité uniquement).

a) Longueur d’une liste :

def longueur(L):
    compteur = 0
    for _ in L:
        compteur += 1
    return compteur

b) Appartenance d’un élément à une liste :

def appartient(x, L):
    for elem in L:
        if elem == x:
            return True
    return False

c) Nombre d’occurrences :

def nb_occurrences(x, L):
    compteur = 0
    for elem in L:
        if elem == x:
            compteur += 1
    return compteur
Correction

a)

def longueur(L):
    return 0 if L == [] else 1 + longueur(L[1:])

Cas de base : la liste vide a une longueur de 0. Appel récursif : 1 plus la longueur du reste.

b)

def appartient(x, L):
    return False if L == [] else (L[0] == x) or appartient(x, L[1:])

Cas de base : x n’est pas dans la liste vide. Sinon, soit le premier élément est x, soit x appartient au reste.

c)

def nb_occurrences(x, L):
    return 0 if L == [] else (1 if L[0] == x else 0) + nb_occurrences(x, L[1:])

On ajoute 1 si le premier élément est x, puis on compte dans le reste.


Exercice 6 : fonctions pures et effets de bord

Pour chaque fonction, indiquez si elle est pure ou impure. Justifiez.

a)

def double(x):
    return 2 * x

b)

compteur = 0
def incrementer():
    global compteur
    compteur += 1
    return compteur

c)

def ajouter(x, L):
    L.append(x)
    return L

d)

def ajouter_pur(x, L):
    return L + [x]
Correction

a) Pure. double(3) renvoie toujours 6, sans modifier quoi que ce soit en dehors de la valeur de retour.

b) Impure. La fonction modifie la variable globale compteur (effet de bord). Deux appels successifs ne renvoient pas la même valeur.

c) Impure. append modifie la liste L en place (effet de bord). Après l’appel, la liste passée en argument a changé.

d) Pure. L’opérateur + crée une nouvelle liste sans modifier L. La liste originale reste intacte.

La différence entre c) et d) est fondamentale : en programmation fonctionnelle, on crée de nouvelles structures plutôt que de modifier les existantes.


Exercice 7 : la fonction forall (exercice guidé)

On souhaite écrire une fonction forall(pred, L) qui renvoie True si tous les éléments de L vérifient le prédicat pred.

Données de test :

pair = lambda x: x % 2 == 0
ex1 = [4, 4, 2, 0, 1]   # attendu : False
ex2 = []                  # attendu : True
ex3 = [4, 4, 2, 0, 8]   # attendu : True

a) Écrivez forall avec une boucle for. C’est la version la plus naturelle : dès qu’un élément ne vérifie pas le prédicat, on renvoie False.

b) Écrivez forall en version récursive pure (sans boucle, sans variable). Quel est le cas de base ? Comment se réduit le problème ?

c) Écrivez forall avec map et all. Quelle est le rôle de chacune de ces fonctions ?

d) Écrivez forall avec une compréhension de liste et all.

e) Écrivez forall avec filter et len.

Correction

a) Avec une boucle (impératif) :

def forall(pred, L):
    for x in L:
        if not pred(x):
            return False
    return True

On parcourt la liste ; dès qu’un élément ne vérifie pas, on renvoie False. Si la boucle se termine, tous vérifient.

b) Récursif pur (fonctionnel) :

def forall(pred, L):
    return True if L == [] else pred(L[0]) and forall(pred, L[1:])

Cas de base : la liste vide → True (tous les éléments d’un ensemble vide vérifient toute propriété). Appel récursif : le premier élément doit vérifier le prédicat et tous les suivants aussi.

c) Avec map et all :

def forall(pred, L):
    return all(map(pred, L))

map(pred, L) applique pred à chaque élément, produisant une séquence de booléens. all renvoie True si tous les booléens sont True.

d) Avec compréhension et all :

def forall(pred, L):
    return all(pred(x) for x in L)

Équivalent à la version map, avec un générateur au lieu de map.

e) Avec filter et len :

def forall(pred, L):
    return len(list(filter(pred, L))) == len(L)

On filtre les éléments vérifiant le prédicat. Si la liste filtrée a la même longueur que l’originale, tous vérifient.


Exercice 8 : synthèse – trois paradigmes sur un même problème

On dispose d’une liste d’enregistrements représentant des élèves :

eleves = [
    {"nom": "Alice", "notes": [15, 12, 18]},
    {"nom": "Bob", "notes": [8, 14, 9]},
    {"nom": "Clara", "notes": [16, 17, 19]},
    {"nom": "David", "notes": [11, 7, 13]},
]

On souhaite obtenir la liste des noms des élèves dont la moyenne est supérieure ou égale à 14.

Résultat attendu : ['Alice', 'Clara']

a) Écrire une solution en style impératif (boucle, variable accumulatrice).

b) Écrire une solution en style fonctionnel (avec filter, map, lambda), puis en compréhension de liste.

c) Écrire une solution en style objet (avec une classe Eleve disposant des méthodes moyenne et est_bon).

d) Comparer les trois approches : laquelle est la plus lisible ? La plus courte ? La plus facilement testable ?

Correction

a) Impératif :

def bons_eleves(eleves):
    resultat = []
    for e in eleves:
        somme = 0
        for n in e["notes"]:
            somme += n
        moyenne = somme / len(e["notes"])
        if moyenne >= 14:
            resultat.append(e["nom"])
    return resultat

Variables modifiables (resultat, somme), boucles for, affectations.

b) Fonctionnel :

moyenne = lambda e: sum(e["notes"]) / len(e["notes"])
bons = list(map(lambda e: e["nom"],
                filter(lambda e: moyenne(e) >= 14, eleves)))

Composition de filter (sélectionner) et map (transformer). Pas de variable modifiable.

En compréhension :

bons = [e["nom"] for e in eleves
        if sum(e["notes"]) / len(e["notes"]) >= 14]

c) Objet :

class Eleve:
    def __init__(self, nom, notes):
        self.nom = nom
        self.notes = notes

    def moyenne(self):
        return sum(self.notes) / len(self.notes)

    def est_bon(self, seuil=14):
        return self.moyenne() >= seuil

objets = [Eleve(e["nom"], e["notes"]) for e in eleves]
bons = [e.nom for e in objets if e.est_bon()]

Données et traitements encapsulés dans la classe.

d) La compréhension de liste (fonctionnel pythonique) est la plus courte et souvent la plus lisible en Python. L’approche objet est la plus facilement testable (on peut tester Eleve.moyenne() et Eleve.est_bon() indépendamment). L’approche impérative est la plus explicite pas à pas, mais la plus verbeuse. Les trois paradigmes produisent le même résultat ; le choix dépend du contexte et de la lisibilité recherchée.