Paradigmes de programmation



Langages de programmation

Un langage de programmation a besoin :

  • des règles de grammaire qui définissent la syntaxe des expressions ;
  • d’une sémantique qui définit le sens des expressions.

Un langage peut être :

  • interprété : un interpréteur lit et analyse le code séquentiellement, le traduit en langage machine et lance son exécution.
  • compilé : un compilateur lit et analyse le code puis le traduit en langage machine. Par la suite l’exécutable généré peut être lancé.

Remarque : Python un langage interprété mais le code n’est pas directement traduit dans le langage machine de l’ordinateur sur lequel le programme est lancé mais dans le langage machine d’une machine virtuelle (bytecode). Dans un second temps, ce langage machine est interprété par le logiciel.

Un compilateur n’est pas obligé d’effectuer une lecture séquentielle du fichier contenant le code ; il peut donc davantage l’analyser (recherche d’erreurs de typage par exemple) et effectuer des optimisations.

Paradigme de programmation

On appelle paradigme de programmation une façon de se représenter un problème informatique et sa résolution.

Le paradigme choisi a une très grande influence sur la manière de concevoir un programme, il constitue donc un des critères principaux pour le choix d’un langage.

Il existe un grand nombre de paradigmes de programmation, le programme de la NSI se concentre sur la programmation impérative, la programmation fonctionnelle et la programmation objet.

Certains langages sont très liés à certains paradigmes mais la plupart permettent de mettre en œuvre plusieurs paradigmes ; c’est le cas de Python :

Programmation impérative

Wikipedia
En programmation impérative les opérations sont décrites en séquences d’instructions, dont l’ordre est déterminé par le programmeur, et donc l’objectif est la modification de l’état de la mémoire (variables).

Les « langages impératifs » sont les plus répandus parmi l’ensemble des langages de programmation et sont ceux qui sont les plus proches du fonctionnement des processeurs : exécuter une suite d’instructions élémentaires.

Les instructions de base de ce paradigme sont :

  • la séquence d’instructions : les instructions sont exécutées les unes après les autres ;
  • l’assignation ou affectation : on stocke ou on modifie une information en mémoire (dans une variable) ;
  • l’instruction conditionnelle : un bloc d’instruction n’est effectué que si une certaine condition est réalisée ;
  • la boucle : une séquence d’instructions est répétée un certain nombre de fois (boucle Pour) ou jusqu’à ce qu’une condition soit réalisée.
  • les branchements : la séquence d’instruction est transférée à un autre endroit dans le code. De nombreux langages incluent un goto permettant de réaliser un branchement ou la possibilité d’écrire des fonctions — qui ne sont dans ce cas qu’une suite d’instructions (et donc n’ont aucun sens mathématique).
La programmation impérative s’appuie sur le modèle des machines à états, avec une mémoire centrale et des instructions qui modifient son état grâce à des affectations successives. Cela nécessite pour le programmeur d’avoir à tout instant un modèle exact de l’état de la mémoire que le programme modifie.

Mots clés pour repérer un langage impératif

  • affectation, mémoire, instruction, boucle, mutabilité, effets de bord, …

Exemples de langages impératifs

  • Langage machine, C, Bash, Perl, Tcl, Python, PHP, Java, JavaScript, …

Usage typique

  • Lorsque le programme à réaliser s’exprime sous forme d’actions à réaliser. Les actions reflètent les changements dans l’environnement (mémoire) du programme.
  • Dans le cadre de la programmation système, pour une gestion manuelle, potentiellement fine, de l’allocation mémoire.

Programmation fonctionnelle

Wikipedia
En programmation fonctionnelle un programme est considéré comme l’application (au sens mathématique du terme) d’une fonction à l’ensemble des données que le programme doit manipuler. Ce programme donne donc toujours le même résultat pour les mêmes données (pas d’effet de bord).
  • La programmation fonctionnelle s’affranchit de façon radicale des effets de bord en interdisant toute opération d’affectation.
  • La manipulation de variables est remplacée par la composition de fonctions (boîtes noires imbriquées les unes dans les autres).
  • Les langages purement fonctionnels remplacent les boucles par la récursivité.
  • Les langages fonctionnels mettent tous en œuvre une gestion automatique de l’allocation mémoire.
  • Dans les langages fonctionnels les fonctions sont des citoyennes de première classe : une fonction peut recevoir une fonction comme argument, une fonction peut retourner une fonction.

Remarque

  • La mise en œuvre des langages fonctionnels fait un usage sophistiqué de la pile (cf. chapitre 1 sur la récursivité et chapitre 8 sur la structure de pile) car, afin de s’affranchir de la nécessité de stocker des données temporaires dans des tableaux, ils font largement appel à la récursivité. L’une des multiples techniques pour rendre la compilation de la récursivité plus efficace est une technique dénommée récursion terminale (en anglais : tail-recursion), qui consiste à accumuler les résultats intermédiaires dans une case mémoire de la pile et à la passer en paramètre dans l’appel récursif. Ceci permet d’éviter d’empiler les appels récursifs dans la pile en les remplaçant par une simple succession de sauts. Le code généré par le compilateur est alors similaire à celui généré par une boucle en impératif.

  • La programmation fonctionnelle éliminant les effets de bord (fonctions pures), il est plus facile de faire effectuer des calculs en parallèle aux programmes développés dans un style fonctionnel pur.

Mots clés pour repérer un langage impératif

  • déclaratif, expressions, immutabilité, ordre supérieur, $\lambda$-calcul, fonction pure, …

Exemples de langages fonctionnels

  • Haskell, Lisp, Scheme, OCaml, …

Usage typique

  • Manipulations de haut niveau ;
  • Calculs algébriques, abstraits ;
  • Écriture de compilateurs.

Comparaison des deux styles de programmation

Exercice 1 : illustration des différences entre les style impératif et fonctionnel

Définir une fonction qui réalise la somme des nombres d’une liste passée en argument,

  1. En style impératif.
  2. En style fonctionnel.
  1. Style impératif

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    from typing import List
    
    def somme(liste: List[float]) -> float:
        """
        Calcule la somme des éléments de la liste liste.
        """
        somme = 0
        for nbre in liste:
            somme = somme + nbre
        return somme
    Cette fonction manipule la variable locale somme et utilise une boucle for. Le paradigme utilisé est bien impératif.

  2. Style fonctionnel

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    from typing import List
    
    def somme(liste: List[float]) -> float:
        """
        Calcule la somme des éléments de la liste liste.
        """
        if len(liste) == 0:
            return 0
        else:
            return liste[0] + somme(liste[1:])
    Cette fonction ne manipule aucune variable et n’utilise pas de boucle mais la récursivité. Le paradigme employé est bien fonctionnel.

Remarque : liste[1:] est une sous-liste de liste dans laquelle le premier élément de liste n’est pas présent.

  1. Style fonctionnel avec récursion terminale
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    from typing import List
    
    def somme(liste: List[float], total: float = 0) -> float:
        """
        Calcule la somme des éléments de la liste liste.
        """
        if len(liste) == 0:
            return total
        else:
            return somme(liste[1:], liste[0] + total)
L’utilisation de la récursivité, seule, n’implique pas forcément que le paradigme de programmation utilisé est fonctionnel. Il faut regarder l’ensemble du programme.

Exercice 2 : un exemple de fonction non pure

Écrire une fonction (sans grand intérêt !!!) dont la spécification est la suivante :

1
2
3
4
def elevation_puissance(n: int) -> float:
    """
    Retourne le nombre demandé à l'utilisateur à la puissance n.
    """
1
2
3
4
5
6
7
def elevation_puissance(n: int) -> float:
    """
    Retourne le nombre demandé à l'utilisateur à la puissance n.
    """
    x = float(input("Veuillez entrer le nombre à élever à la puissance n : "))

    return x**n

Il est évident que deux appels successifs à la fonction elevation_puissance avec le même argument (2 par exemple) pourront renvoyer des valeurs différentes puisque celles-ci dépendent de la valeur entrée par l’utilisateur. Cette fonction n’est pas une fonction pure ; sa définition n’est pas acceptable du point de vue de la programmation fonctionnelle.

Exercice 3 : illustration d’un effet de bord

Définir une fonction qui élève au carré chaque élément d’une liste.

  1. En style impératif (avec effet de bord possible).
  2. En style fonctionnel (sans effet de bord).
  1. Style impératif avec effet de bord.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    from typing import List
    
    def liste_carre(liste: List[float]) -> None:
        """
        Élève au carré chaque élément de la liste liste.
        """
        for i in range(len(liste)):
            liste[i] = liste[i]**2
    
    l = [1, 2, 3, 4]
    liste_carre(l)
    La fonction modifie la liste globale liste ! Le problème a pour origine la mutabilité de la structure de données « liste » et le passage d’une référence à la fonction, et non pas une copie de cette liste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from typing import List

def liste_carre(liste: List[float]) -> List[float]:
    """
    Élève au carré chaque élément de la liste liste.
    """
    newList = liste[:]

    def eleve_carre(liste: List[float]) -> List[float]:
        if len(liste) == 0:
            return liste
        else:
            return [liste[0]**2] + eleve_carre(liste[1:])
    
    return eleve_carre(newList)
On copie dans un premier temps la liste et on agit sur cette nouvelle liste. À noter que cette copie n’était dans l’absolu pas nécessaire car l’opérateur + appliqué à des opérandes du type « séquence » génère une nouvelle liste en Python.

Programmation objet

La programmation objet s’inspire du monde réel dans lequel des objets créés à partir de patrons (ou moules) interagissent les uns avec les autres.
Mettre en œuvre ce paradigme nécessite donc de modéliser le problème par une interaction d’objets dont on définit les caractéristiques.
  • Un objet possède un état (des champs ou attributs) et un comportement (des méthodes).
  • Une classe est un patron permettant de fabriquer des objets. Une classe est aussi la définition d’un nouveau type de données.

Mots clés pour repérer un langage objet

  • classes ou prototype, objet, méthode, attribut, champ, statique, encapsulation, héritage, polymorphisme, …

Exemples de langage orientés objets

  • C++, Java, Python, OCaml, …

Usage typique

  • Développement en équipe de logiciels comportant un très grand nombre de lignes de code.