- Langages et programmation -
Paradigmes de programmation
Il existe de nombreux langages de programmation, chacun avec ses spécificités et ses adeptes… Il est néanmoins possibles de les classer selon leur paradigme, selon la façon dont ils manipulent les données et traitent les problèmes (les paragraphes ci-dessous sont inspirés de cette source) :
Les langages les plus classiques suivent un paradigme impératif. Les programmes rédigés dans de tels langages sont constitués de listes d’instructions qui détaillent les opérations que l’ordinateur doit appliquer aux entrées du programme. C, Pascal ou encore les interpréteurs de commandes Unix sont des langages procéduraux
les langages déclaratifs permettent d’écrire la spécification, la description, du problème et laissent l’implémentation du langage trouver une façon efficace de réaliser les calculs nécessaires à sa résolution. SQL fait partie des langages déclaratifs : une requête SQL décrit le jeu de données que l’on souhaite récupérer et le moteur SQL choisit de parcourir les tables ou d’utiliser les index, l’ordre de résolution des sous-clauses, etc…
les programmes orientés objet manipulent des ensembles d’objets (!). Ceux-ci possèdent un état interne et des méthodes qui interrogent ou modifient cet état d’une façon ou d’une autre. Java est un langage orienté objet. C++ et Python gèrent la programmation orientée objet mais n’imposent pas l’utilisation de telles fonctionnalités
Enfin, la programmation fonctionnelle implique de décomposer un problème en un ensemble de fonctions. Dans l’idéal, les fonctions produisent des sorties à partir d’entrées et ne possède pas d’état interne qui soit susceptible de modifier la sortie pour une entrée donnée. Les langages fonctionnels les plus connus sont ceux de la famille ML (Standard ML, OCaml et autres) et Haskell
Certains langages sont clairement associés à un paradigme
Selon sa formation, son goût mais dans l’idéal selon le type de problème qu’il a à résoudre, un programmeur pourra choisir de rédiger son code dans un pradigme l’autre.
C’est le paradigme de programmation le plus classique, celui que nous utilisons régulièrement en Python.
Dans ce paradigme, le programme consiste en un ensemble d’instruction décrivant comment modifier l’état (la mémoire, les relations avec les périphériques…) de l’ordinateur.
On peut, en programmation impérative utiliser les instructions suivantes :
ouvrir le cahier; écrire le cours; fermer le cahier;
)a = 5
if
while
goto
utilisés dans certains langagesLe langage machine (l’assembleur) peut être considéré comme faisant partie de la programmation impérative. Les courts scripts python que nous tapons en cours en font aussi partie bien souvent.
Dans ce paradigme de programmation, on organise nos données en objets dotés d’attributs et de fonctions particulières, leurs méthodes.
Le modèle d’un objet est décrit dans une classe. C’est le modèle abstrait de notre objet. On peut ensuite créer une instance de cet objet : c’est un objet réel, pas une abstraction.
Un Chien abstrait est par exemple un Animal, un Mammifère…: ces caractéristiques peuvent aussi être d’autres classes dont le Chien hérite certaines propriétés (un Chien au même titre que tous les Mammifères a une colonne vertébrale).
Un Chien, quel qu’il soit a un nom, une taille, un poids…: ce sont ses attributs.
Enfin, un Chien est capable de s’asseoir, de s’allonger…: ce sont ses méthodes.
Milou est un chien, dont le nom est “Milou”, la taille \(50\,cm\)…
Lorsque l’on créée une classe, on crée le plus souvent les fonctions/méthodes suivantes :
On pourra ainsi faire :
# Création d'une instance (d'un chien réel)
rayne = nouveau Chien("Gray, White and Black", "Blue and Brown", 18, 16, 30)
# Obtention de la taille de rayne => retourne 18
rayne.get_length()
# Modification du poids rayne => il pèse désormais 31
rayne.set_weight(31)
# On fait s'asseoir rayne
rayne.sit()
En python la POO on crée une classe ainsi :
class Chien :
def __init__(self, color, eyes, height, length, weight) :
"""
Constructeur, prend en argument les valeurs des attributs
"""
self.color = color
self.eyes = eyes
self.height = height
self.length = length
self.weight = weight
def get_color(self) :
"""
Accesseur de l'atribut color
"""
return self.color
def set_color(self, new_color) :
"""
Mutateur de l'attribut color
"""
self.color = new_color
def sit() :
"""
Méthode faisant s'asseoir le chien
"""
# Code
On peut insister sur l’utilisation du mot-clé self
. Celui-ci indique que ce qui suit va s’appliquer à l’instance de l’objet en elle-même, à ce chien précis, pas à tous les Chiens abstraits.
Pour utiliser cette classe, on instancie un objet que l’on peut manipuler :
Cette pratique est impossible dans bien des langages.
L’intérêt de la programmation orientée objet est la possibilité de créer de toute pièce des objets correspondant au problème qui nous est proposé : si je dois créer une interface graphique, j’utiliserai un objet Fenêtre dont les méthodes permettent par exemple d’afficher une image ou du texte, de colorier un pixel, d’interagir avec la souris de l’ordinateur…
Combien de lignes de codes comporte Facebook : plus de 60 millions (voir ici)! L’entretien de tels programmes nécessite des techniques robustes et la seule programmation impérative ou orientée objet montre ses limites lorsque l’on augmente la taille du code.
L’un des soucis majeur est l’effet de bord. Considérons la fonction python suivante :
def calculer(n : int) -> int :
if liste[0] % 2 == 0 :
liste[0] = 1
return 2*n
else :
liste[0] = 0
return n // 2
On remarque que ce code utilise une variable liste
que l’on définit initialement ainsi : liste = [112]
.
Que vaudra calculer(8)
? liste[0]
est égal à 112
, c’est un nombre pair donc :
1
à liste[0]
8
soit 16
Et si l’on réappelle calculer(8)
? Désormais liste[0]
est impair (on l’a modifié lors du premier appel) :
0
à liste[0]
8
soit 4
La même fonction, appelée avec le même argument ne retourne pas le même résultat ! C’est plutôt facheux…
Le soucis est appelé effet de bord (ou side effect en anglais) : la fonction calculer
a, lors de son exécution, modifiée la valeur d’une variable globale, d’une variable qui existe indépendamment d’elle. Imaginez des dizaines, des centaines de fonctions se comportant ainsi dans un code de 60 millions de lignes… Comment trouver l’origine d’une erreur ?
C’est ce point (entre autres) qu’entend régler la programmation fonctionnelle.
Dans ce cadre, une fonction informatique est une boîte noire prenant un argument et renvoyant un résultat. Le même argument renvoie le même résultat. Comme en mathématiques.
Dès lors, le parti-pris de la programmation fonctionnelle est radical : tout est fonction, on s’interdit d’utiliser dans une fonction, la valeur d’une variable définie à l’extérieur sauf si c’est un argument. Quoiqu’il arrive, on s’interdit de modifier cette valeur, ce serait un effet de bord. Certains langages fonctionnels vont jusqu’à interdire les affectations du type a = 5
.
Alors que certains langages sont purement fonctionnels (Lisp, Haskell), d’autres tels que le Python, Kotlin (désormais utilisé par Andoid) ou Scala.
Voici un exemple de fonctions python permettant de doubler tous les nombres d’une liste, en version non fonctionnelle (on modifie la liste de départ) et fonctionnelle (on retourne une nouvelle liste) :
def double_non_fonctionnelle(liste: list) -> None:
for i in range(len(liste)):
liste[i] = 2 * liste[i]
def double_fonctionnelle(liste: list) -> list:
return [2*k for k in liste]
Le code ci-dessous :
liste = [3, 4, 5]
double_non_fonctionnelle(liste)
print(liste)
liste = [3, 4, 5]
liste_bis = double_fonctionnelle(liste)
print(liste)
print(liste_bis)
donnera cela :
[6, 8, 10]
[3, 4, 5]
[6, 8, 10]
On voit que liste
a été modifiée lors du premier appel mais pas lors du second.