Aller au contenu principal

Paradigmes & patterns · L2 · Section 2/4

Fonctionnel

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Fonctionnel

Le style fonctionnel traite un programme comme une composition de transformations : des données entrent, des données sortent, et rien d'autre ne bouge. Les fonctions pures et l'immutabilité rendent les effets explicites, la concurrence plus simple et les tests plus courts. Ce chapitre montre la panoplie (fonctions pures, composition, closures), la transformation concrète d'un code impératif en pipeline, et les coûts réels à mesurer.

Prérequis : Python de base (fonctions, listes, dictionnaires), la page Impératif et objet du même module pour le contraste des styles.

Objectifs d'apprentissage :

  • Reconnaître et écrire des fonctions pures (déterministes, sans effet de bord observable).
  • Transformer une boucle accumulateur en une chaîne map/filter/reduce sans changer le résultat.
  • Composer des fonctions et utiliser les closures pour fabriquer des transformations paramétrées.
  • Séparer le cœur de calcul pur des effets d'I/O, et savoir pourquoi cette frontière aide les tests.
  • Évaluer le coût mémoire des structures immuables et choisir quand muter localement reste raisonnable.

#Fonctions pures : la définition Opérationnelle

Une fonction est pure si, à entrées égales, elle retourne des sorties égales, et si elle ne modifie rien d'observable ailleurs (pas de variable globale, pas d'écriture fichier, pas d'affichage). Le test pratique : appeler deux fois avec les mêmes arguments doit donner le même résultat, et rien autour ne doit avoir changé.

Chargement de l’éditeur...

Les trois transformations fondatrices : map applique une fonction à chaque élément (ici les carrés [1, 4, 9, 16]), filter garde ceux qui satisfont un prédicat (les pairs [2, 4]), reduce fusionne tout en une valeur (la somme 10). Aucune ne modifie la liste de départ : chacune en produit une nouvelle.

#Transformation concrète : de la boucle au pipeline

Un même traitement, la répartition d'une note sur des étudiants avec exclusion des absents et bonus, écrit deux fois. Version impérative avec accumulateur :

Chargement de l’éditeur...

Même calcul, en pipeline fonctionnel : chaque étape est une fonction pure nommée, testable isolément, et l'ordre de lecture correspond au flot des données.

Chargement de l’éditeur...

Les deux versions affichent la même liste : [('alice', 15, 'en-dessous'), ('carla', 18, 'au-dessus')] (moyenne des notes bonus : 16,5 ; Alice est passée de 14 à 15, Carla de 17 à 18). Analysez le gain, étape par étape : presents se teste avec une liste arbitraire sans rien préparer ; avec_bonus se teste sur un seul couple ; une régression dans le calcul de la moyenne se localise en regardant moyenne et nulle part ailleurs. Dans la version impérative, la même erreur peut habiter n'importe laquelle des trois boucles, qui partagent les mêmes variables.

La lecture du pipeline se fait de l'intérieur vers l'extérieur, comme une composition mathématique : annoter ∘ avec_bonus ∘ presents. Une écriture équivalente, compose(f, g)(x) = f(g(x)), est développée dans la section composition ci-dessous.

#Composition et closures

Une closure est une fonction qui capture son environnement de définition ; une composition enchaîne des fonctions comme des segments de tuyau. Ensemble, elles permettent de fabriquer des transformations paramétrées sans classes ni état global.

Chargement de l’éditeur...

Lecture : compose(plafonne(15), seuil(5)) construit une fonction unique qui filtre à 5 puis borne à 15. Le résultat [12, 7, 15] : le 20 est passé le seuil puis ramené à 15, le 3 est parti au premier étage, le 1 aussi. seuil(5) et plafonne(15) sont des closures : chacune capture son paramètre et retourne une fonction spécialisée. C'est l'équivalent fonctionnel de la fabrique ou de la stratégie côté objet : on paramètre un comportement en le fabriquant, sans classe.

#Le coût réel de l'immutabilité

Chaque étape d'un pipeline parcourt et recrée une collection : sur une liste de n éléments et k étapes, l'ordre de grandeur mémoire et temps est O(k·n). Pour des données de taille raisonnable, c'est indolore et la lisibilité gagne. Pour des volumes importants, trois réflexes :

  • Fusionner les passes : une seule compréhension qui filtre et transforme évite une liste intermédiaire.
  • Générateurs : (...) au lieu de [...] produit les éléments à la demande, en O(1) mémoire par étape chaînée.
  • Mutation locale encadrée : muter une liste à l'intérieur d'une fonction qui la construit ne casse pas la pureté, tant que rien ne fuit vers l'extérieur.
Chargement de l’éditeur...

Sur les tests : les trois versions rendent exactement la même sortie. La version fusionnée fait une traversée au lieu de trois ; la version générateur ne matérialise qu'une liste. Le choix n'est pas dogmatique : la version trois étapes séparées reste la plus lisible quand chaque étape exprime une règle métier distincte, la version fusionnée se justifie quand le volume devient sensible.

#Exercice : Pipeline de traitement de données

Implémentez un pipeline de traitement de données en utilisant des fonctions pures et la composition pour transformer une liste de dictionnaires représentant des utilisateurs.

#Instructions

  1. Créez des fonctions pures pour chaque étape de transformation :
    • filter_adults(users) : filtrer les utilisateurs de 18 ans et plus.
    • normalize_names(users) : normaliser les noms (capitaliser la première lettre).
    • add_age_group(users) : ajouter un groupe d'âge ('young', 'adult', 'senior').
    • sort_by_name(users) : trier par nom.
  2. Implémentez une fonction compose pour composer plusieurs fonctions en une seule.
  3. Appliquez le pipeline à une liste d'utilisateurs.

#Correction guidée

pythonpython

1def filter_adults(users):2    return [user for user in users if user['age'] >= 18]3 4def normalize_names(users):5    return [{'name': user['name'].capitalize(), 'age': user['age']} for user in users]6 7def add_age_group(users):8    def get_age_group(age):9        if age < 30:10            return 'young'11        elif age < 60:12            return 'adult'13        else:14            return 'senior'

Points à vérifier dans votre solution :

  1. Ordre de composition : compose(sort_by_name, add_age_group, normalize_names, filter_adults) applique d'abord filter_adults (argument le plus à droite) puis remonte vers sort_by_name. C'est la convention mathématique f(g(x)). Les deux ordres produisent ici des sorties correctes, mais filtrer d'abord évite de normaliser et trier des éléments qui seront jetés.
  2. Pureté de chaque étape : aucune fonction ne modifie users. Comptez les tests possibles : filter_adults sur une liste avec et sans mineur, normalize_names sur un nom tout en minuscules, sans aucune préparation d'état.
  3. Sortie attendue : Alice young 25, Charlie adult 35, Diana senior 65 (bob, 17 ans, est éliminé dès la première étape, et l'ordre alphabétique vient du tri final).

Méthode de vérification : exécuter le pipeline, puis exécuter les étapes une à une à la main sur un cas simple à trois utilisateurs ; comparer les listes intermédiaires (après filter_adults, après normalize_names, etc.) et le résultat final. Toute divergence isole l'étape fautive sans relecture globale du pipeline.

Chargement de l’éditeur...

Sortie attendue : Alice (young, 25), Charlie (adult, 35), Diana (senior, 65). Bob, 17 ans, a été filtré à la première étape ; les noms ont été capitalisés à la deuxième ; le groupe d'âge ajouté à la troisième ; le tri alphabétique appliqué en dernier.

#Mini-quiz

Une fonction « pure » signifie :
Une fonction « pure » signifie :
compose(f, g)(x) vaut :
compose(f, g)(x) vaut :
Quelle écriture économise les listes intermédiaires ?
Quelle écriture économise les listes intermédiaires ?