Aller au contenu principal

Algorithmiques élémentaires & pensée computationnelle · L1 · Section 2/6

Abstraction

Progression

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

#Abstraction

L'abstraction simplifie des systèmes complexes en cachant les détails non essentiels pour se concentrer sur l'intention et les interfaces.

#Prérequis

Savoir écrire une fonction Python avec paramètre et valeur de retour (voir fonctions).

#Objectifs d'apprentissage

  • Distinguer le « quoi » (spécification) du « comment » (implémentation).
  • Reconnaître les trois niveaux d'abstraction : données, procédures, contrôle.
  • Écrire un contrat d'interface : préconditions, postconditions, invariants.
  • Distinguer fonction totale et fonction partielle, et situer une précondition dans le domaine de définition.
  • Utiliser la curryfication pour ramener une interface à l'arité attendue.
  • Utiliser l'encapsulation pour laisser l'implémentation évoluer sans casser les appelants.

#Qu'est-ce que l'abstraction ?

L'abstraction sépare le « quoi » du « comment ». Elle réduit la complexité, permet de raisonner sur un comportement attendu et autorise plusieurs implémentations derrière la même interface. Le test décisif : peut-on remplacer l'intérieur sans changer les appelants ? Si oui, l'abstraction est réussie.

#Niveaux d'abstraction

Les données sont décrites par des types et des structures (float, listes, dictionnaires) qui fixent ce que l'on peut faire sans exposer la représentation interne.

Les procédures regroupent des fonctions ou des méthodes dont on retient le contrat (sort, append, send). L'algorithme exact peut changer tant que le contrat reste vrai.

Le contrôle englobe les motifs d'expression du parcours et de l'orchestration (boucles, itérateurs, map/filter, reduce, futures) qui expriment l'intention sans détailler la mécanique.

#Exemples rapides

Un set assure l'unicité et permet de tester l'appartenance en temps quasi constant, sans révéler qu'il repose sur une table de hachage. L'appel à liste.sort() se contente de promettre un ordre croissant ; la bibliothèque choisit l'algorithme le plus adapté (Timsort). Quand on écrit for x in it, on consomme un itérateur étape par étape sans manipuler d'indices ni d'état interne.

#Interfaces et invariants

Une interface définit un contrat : des préconditions (ce qui doit être vrai avant l'appel), des postconditions (ce qui est garanti après) et parfois des invariants (ce qui reste vrai en permanence, comme une pile toujours LIFO). Tant que le contrat reste vrai, l'implémentation peut évoluer. L'encapsulation garde les détails internes privés pour éviter que le code appelant ne dépende de choix susceptibles de changer.

pythonpython

1class Stack:2    """Pile LIFO. Invariant : l'élément retiré est toujours le dernier ajouté."""3 4    def __init__(self):5        self._a = []  # détail interne : personne d'autre ne le voit6 7    def push(self, x):8        """Ajoute x au sommet. Postcondition : peek() == x."""9        self._a.append(x)10 11    def pop(self):12        """Retire et renvoie le sommet.13        Précondition : la pile n'est pas vide (sinon IndexError).14        """

L'utilisateur de Stack ne sait pas qu'il y a une liste dessous. Demain, la classe peut passer à une liste chaînée : aucun appelant ne changera. Le préfixe _ signale l'attribut privé par convention ; rien ne l'impose au runtime, c'est le contrat social de Python.

#Fonctions totales et partielles : le contrat, version mathématique

Les notions de précondition et de postcondition ont un fondement précis. En mathématiques, une fonction ff d'un ensemble AA vers un ensemble BB est une relation telle que chaque élément admette au plus une image :

(xRy et xRz)    y=z.(x\,R\,y \text{ et } x\,R\,z) \implies y = z.

On note f(x)=yf(x) = y le fait que xx est en relation avec yy. Deux ensembles se distinguent alors :

  • le domaine de définition dom(f)={aA:bB, f(a)=b}A\mathrm{dom}(f) = \{\, a \in A : \exists b \in B,\ f(a) = b \,\} \subseteq A, c'est-à-dire l'ensemble des entrées pour lesquelles ff est définie ;
  • l'image (ou codomaine) Im(f)={bB:aA, f(a)=b}B\mathrm{Im}(f) = \{\, b \in B : \exists a \in A,\ f(a) = b \,\} \subseteq B, c'est-à-dire l'ensemble des sorties effectivement atteintes.

Une fonction est totale si dom(f)=A\mathrm{dom}(f) = A, et partielle si dom(f)A\mathrm{dom}(f) \subsetneq A. Le cours signale que les termes « application » et « transformation » sont synonymes de fonction, à cette précision près : une application est une fonction totale, dont tout élément de l'ensemble de départ a une image.

Les propriétés d'une fonction se définissent de la même façon rigoureuse :

  • injective : deux entrées distinctes ont des images distinctes ;
  • surjective : tout élément de BB est atteint, c'est-à-dire Im(f)=B\mathrm{Im}(f) = B ;
  • bijective : injective et surjective à la fois.

Le cours ajoute une remarque utile : lorsque A=B\lvert A \rvert = \lvert B \rvert (ensembles finis de même cardinal), ces trois propriétés coïncident. Autrement dit, sur des ensembles de même taille, prouver l'une des trois suffit à obtenir les deux autres — un raccourci de démonstration précieux, et l'énoncé exact de ce qui fait d'une représentation un encodage réversible.

#Curryfication : réduire l'arité d'une interface

Une interface à plusieurs paramètres peut toujours se ramener à une composition d'interfaces à un seul paramètre. C'est la curryfication : à partir d'une fonction f:A×BCf : A \times B \to C, on définit

Curry(f):A(CB),\mathrm{Curry}(f) : A \to \left(C^{B}\right),

qui associe à chaque aAa \in A une fonction de BB vers CC, de sorte que

f(a,b)=Curry(f)(a)(b).f(a, b) = \mathrm{Curry}(f)(a)(b).

L'exemple classique est l'addition. À partir de add : ℕ × ℕ → ℕ, on introduit pour chaque aa une fonction adda:NN\mathrm{add}_a : \mathbb{N} \to \mathbb{N} définie par adda(b)=a+b\mathrm{add}_a(b) = a + b, et l'on a add(a,b)=adda(b)\mathrm{add}(a, b) = \mathrm{add}_a(b).

En Python, c'est exactement ce que fait functools.partial, et le résultat est une fonction à laquelle il reste des arguments à fournir :

pythonpython

1from functools import partial2 3def ajouter(a, b):4    return a + b5 6ajouter_10 = partial(ajouter, 10)   # Curry(ajouter)(10)7print(ajouter_10(5))                # 158print(list(map(ajouter_10, [1, 2, 3])))  # [11, 12, 13]

Pourquoi c'est une abstraction, et non une curiosité. Une fonction curryfiée est une fonction dont on peut fixer une partie du contrat et transmettre le reste comme une valeur. C'est ce qui permet d'écrire map(ajouter_10, ...) là où une fonction à deux arguments serait refusée : l'interface a été ramenée à l'arité attendue par le consommateur. Le cours note que cette technique est au fondement des langages fonctionnels (Scheme, Lisp, Haskell) et qu'elle se généralise aujourd'hui à la plupart des langages.

#Abstraction et composition

Les systèmes s'assemblent par couches successives : matériel, système d'exploitation, runtime, bibliothèque, application. Chaque palier expose un langage plus expressif que le précédent. La loi de Déméter rappelle de limiter les dépendances transverses : on discute avec ses voisins directs, pas avec toute la chaîne interne. obj.a.b.c.d est un symptôme : chaque point supplémentaire couple l'appelant à un détail d'implémentation d'un autre étage.

#Exercices

  1. Concevoir l'ADT Queue (file) en listant les opérations autorisées, les préconditions et postconditions, puis en proposant deux implémentations (tableau circulaire, liste chaînée).
  2. Définir une interface Repository avec les opérations CRUD et écrire une version en mémoire puis une version persistant en fichier, en gardant les mêmes signatures.
  3. Reprendre une boucle for explicite et la réécrire avec map / filter pour illustrer la montée en abstraction sur le contrôle.
  4. Pour pop sur une pile, division(a, b) et get(id) sur un dépôt : donnez le domaine de définition de chacune, dites si elle est totale ou partielle, puis rendez-la totale de deux façons (valeur sentinelle, exception documentée).
  5. Curryfiez une fonction puissance(base, exposant) pour obtenir une fonction carre et une fonction cube, puis utilisez-en une avec map.

#Éléments de correction

  1. Opérations : enqueue(x) (postcondition : la file contient x en queue), dequeue() (précondition : file non vide; postcondition : renvoie l'élément de tête et le retire), front() (précondition : file non vide), is_empty(), size(). Invariant : l'ordre de sortie est exactement l'ordre d'entrée (FIFO). Implémentation A : tableau et deux indices avec rebouclage modulo capacité. Implémentation B : liste chaînée avec tête et queue. Les deux satisfont le même contrat.
  2. Repository expose get(id), put(obj), delete(id), list_all(). Version mémoire : un dictionnaire. Version fichier : sérialisation JSON à chaque écriture. Les signatures et les garanties (un get sur un identifiant inexistant renvoie None) sont identiques : c'est tout l'intérêt.
  3. resultat = [f(x) for x in valeurs if p(x)] se réécrit resultat = list(map(f, filter(p, valeurs))). La version déclarative nomme l'intention (transformer ceux qui passent le filtre) et laisse le parcours implicite.
  4. pop : domaine = les états de pile non vides ; partielle. division(a, b) : domaine = les couples avec b0b \neq 0 ; partielle. get(id) : domaine = les identifiants existants ; partielle. Pour rendre totale : soit une valeur sentinelle (None, 0), soit une exception documentée (IndexError, ZeroDivisionError, KeyError). Les deux élargissent le domaine au type d'entrée entier, mais la sentinelle masque l'erreur dans le flux de valeurs tandis que l'exception la rend impossible à ignorer : le choix dépend de si l'absence est un cas normal ou une faute d'appel.
  5. from functools import partial puis carre = partial(puissance, 2) et cube = partial(puissance, 3); on a puissance(b, e) = partial(puissance, b)(e). Usage : list(map(carre, [1, 2, 3, 4])) donne [1, 4, 9, 16]. L'intérêt est qu'une fonction à deux arguments devient une fonction à un argument, donc acceptable là où map attend une interface unaire.

#Quiz

Quel énoncé capture le mieux l'idée d'abstraction ?
Quel énoncé capture le mieux l'idée d'abstraction ?
Une classe Pile expose push, pop, peek et documente l'invariant LIFO. On remplace la liste interne par une liste chaînée. Que doivent devenir les programmes appelants ?
Une classe Pile expose push, pop, peek et documente l'invariant LIFO. On remplace la liste interne par une liste chaînée. Que doivent devenir les programmes appelants ?
Que désigne le domaine de définition d'une fonction partielle ?
Que désigne le domaine de définition d'une fonction partielle ?