Langages de programmation

Les élèves doivent savoir
  • Mettre en évidence un corpus de constructions élémentaires.
  • Repérer, dans un nouveau langage de programmation, les traits communs et les traits particuliers à ce langage.
  • Prototyper une fonction.
  • Décrire les préconditions sur les arguments.
  • Décrire des postconditions sur les résultats.
  • Utiliser des jeux de tests.
  • Utiliser la documentation d’une bibliothèque.

1.    Constructions élémentaires

Machine de Turing

En 1936, le mathématicien anglais Alan Turing publie un article intitulé « On computable numbers, with an application to the entscheidungsproblem » (sur les nombres calculables avec une application au problème du choix). Il cherche à répondre à la question posée en 1928 par un autre mathématicien, David Hilbert, qui se demandait s’il existait un algorithme qui puisse dire si une proposition quelconque énoncée dans un système logique était ou non valide.

Pour cela Alan Turing décrit un modèle très simple : un ruban, de longueur infinie, comportant des cases (et précurseur de la mémoire des ordinateurs) dans lesquelles on ne peut écrire que trois valeurs : les deux chiffres binaires 0 et 1, ainsi que le vide. Une tête de lecture, qui peut se déplacer à droite ou à gauche, sera pilotée par l’algorithme (suite d’instructions).

L’algorithme sera un ensemble de règles qui vont indiquer ce qu’il faut faire quand la tête de lecture lit le vide, 0 ou 1. À chaque lecture correspond une écriture et un déplacement. Chaque instruction se compose donc de 3 étapes :

  • Ce que lit la tête
  • Ce qu’elle doit écrire
  • Dans quel sens elle doit se déplacer

Les règles de calculs sont alors exprimées sous la forme d’un diagramme comportant les différentes étapes de l’algorithme.

Voici le diagramme correspondant à une addition de 1 à un nombre binaire (nombres composé des chiffres 0 et 1) écrit sur la bande, à raison d’un chiffre par case :

Addition de 1 à un binaire

Les flèches après les barres obliques indiquent les déplacements à gauche ou à droite. Les instructions sont lues de la façon suivante : « vide/ecrit vide/➞ » se lit « si la case est vide, écrit vide (donc rien) puis déplace la tête vers la droite (ou la bande vers la gauche, il faut clarifier cela avant de tester le programme) ».

Vous pourrez trouver des exemples d’algorithmes détaillés ainsi qu’un simulateur de machine de Turing sur le site Intersite.info : https://interstices.info/comment-fonctionne-une-machine-de-turing/ et une histoire détaillée de cette machine sur le site du journal Pour la Science : https://www.pourlascience.fr/sd/histoire-techniques/la-machine-de-turing-2880.php .

On dira d’un langage de programmation qu’il est « Turing Complet » s’il permet de programmer n’importe quelle machine de Turing. C’est le cas de tous les langages de programmation usuels (Pacal, C, Python, Java…), mais aussi du HTML+CSS ou du SQL.

Paradigmes de programmation

Il y a différentes manières d’approcher la résolution d’un problème par la programmation, ce sont les « paradigmes ». Dans un langage de programmation choisi, il peut y avoir plusieurs paradigmes utilisables. Le choix dépend donc du langage choisi et des décisions prises par l’équipe de programmeurs au moment de la conception du programme.

De façon générale, les paradigmes de programmation se subdivisent en deux types : déclaratifs et impératifs.

Les langages déclaratifs, non étudiés en détail en spécialité NSI en première, sont eux-mêmes divisés en deux catégories : fonctionnel et logique. De façon générale, un langage déclaratif repose sur des prédicats (déclarations dont il faudra vérifier l’état pour savoir si elles sont vraies, fausses ou indéterminées). Dans ces langages on laisse la machine déterminer de quelle façon elle va résoudre le problème après lui avoir transmis les règles pour les résoudre.

Le paradigme fonctionnel utilise l’évaluation de formules mathématiques dont les résultats sont réutilisés pour d’autres calculs (exemple : Lisp, Ruby, Camel). Un paradigme logique va utiliser des requêtes et des formules pour effectuer des recherches sur des ensembles (exemple : SQL).

Les langages impératifs sont utilisés de façon plus courante, car ils sont plus faciles à concevoir du fait de leur approche par étapes successives , comme une recette de cuisine. On les classe en deux grandes familles qui sont complémentaires et souvent présentes ensemble dans les langages modernes : procédurales et objet.

Les paradigmes procéduraux utilisent un ensemble d’états sur lesquels on va effectuer des opérations, ainsi que des boucles pour répéter ces opérations (exemple : C, Pascal, Fortan). Dans le paradigme objet, on définit des structures (les objets) qui sont regroupées par classes et vont posséder certaines propriétés et procédures (nommées méthodes) associées (exemples : C++, Java).

Python est un langage qui utilise des paradigmes impératifs, procéduraux et objets. Il comprend toutefois aussi des éléments fonctionnels, ce qui en fait un langage très complet et puissant, très utilisé dans la recherche scientifique.

Éléments de base d’un langage de programmation

  • Instructions

Les langages de programmation sont constitués d’une suite d’instructions qui traduisent les ordres donnés à l’ordinateur. Ces instructions sont délimitées de façon différente selon les langages. Parfois le passage à la ligne est suffisant, parfois il faudra rajouter une ponctuation en fin de ligne (généralement le point-virgule « ; »). Ces éléments de syntaxe sont souvent la source des erreurs dans les programmes et il convient d’y faire très attention. Un bon environnement de développement (IDE) pourra vous aider en pointant ces erreurs ou en les corrigeant de façon automatique.

  • Variables et constantes

Les opérations s’effectuent ensuite sur des valeurs qui peuvent être des variables (qui changent au cours du programme) ou des constantes (fixées une fois pour toute en début de programme) qu’il faut déclarer. Cette déclaration entraîne un stockage de la valeur dans un emplacement en mémoire qui sera enregistré par le programme.

La plupart des langages demandent de préciser de quel type est la valeur qui doit être stockée. Ce type doit être précisé au moment de la déclaration de la constante ou de la variable.

Par exemple, en C, il existe de très nombreux types en fonction de ce qu’on veut y stocker. En voici quelques-uns :

intEntier de valeur maximale 231-1 (en 32 bit)
floatNombre réel en simple précision
doubleNombre réel en double précision
charcaractère

Python est un langage en typage faible, ce qui veut dire qu’il n’est pas nécessaire d’indiquer de quel type il s’agit, mais que c’est l’interpréteur Python qui va déterminer de quel type on parle.

Voici une comparaison entre deux programmes où l’on donne une valeur à une variable a, on copie cette valeur dans une autre variable b, on modifie a et on affiche le résultat :

En C
Int main() 
{
  int a=5 ;
  int b=a ;
  a=a-1 ;
  printf("a=%d et b=%d",a,b) ;
  return(0)
}

Résultat

a=4 et b=5
En Python
a = 5 
b = a
a = a-1
print("a=", a, "b=", b)

Résultat

a= 4 b= 5

Les langages de programmation ont des façons différentes d’accéder à ces emplacements mémoire. Une mauvaise compréhension de la façon dont le langage gère cela peut être une source d’erreur.

Par exemple, en python, les listes ne sont pas copiées par défaut quand on les affecte à une autre variable, cela peut donner des surprises comme dans le cas ci-dessous :

Code Python
a = [5] 
b = a
a[0] = a[0]-1
print("a=", a, "b=", b)

Résultat

a= [4] b= [4]

On constate ici que b n’est pas une véritable copie de a, car il conserve le même contenu que la variable a si celle-ci est modifiée. En fait, pour économiser de la mémoire, Python ne fait que faire pointer b vers l’emplacement mémoire de a ! Pour faire une véritable copie de a il aurait fallu écrire : b = a.copy().

Les listes n’existent pas dans tous les langages de programmation et les tableaux ou dictionnaires non plus. Le passage d’un langage de programmation à un autre oblige donc à modifier (parfois fortement) les programmes pour s’adapter à ce qui est possible et, surtout, efficace avec le langage choisi.

  • Nommer les variables et constantes

Pour le nommage des variables et des constantes, il est important d’être assez explicite et de ne pas utiliser systématiquement une seule lettre (a, b, i…).

En Python, une variable ne doit pas commencer par un chiffre, ne doit pas contenir de caractères accentués et ne doit pas contenir d’espaces (que l’on peut remplacer par le caractère souligné – underscore – : « _ »).

Il est en outre d’usage de n’utiliser que des minuscules pour les variables, les premières lettres en majuscules étant réservées aux classes des objets (voir plus loin) et les mots en majuscule complète sont réservés aux constantes :

  • plateau_de_jeu : c’est une variable
  • PI : ce sera une constante
  • Fenetre : c’est le nom d’une classe (avec plusieurs mots : FenetrePrincipale)
  • Les boucles

Les boucles sont un élément clé d’un langage, car elles permettent de répéter un grand nombre de fois une suite d’instructions. Elles sont associées à un compteur qui permet de déterminer le nombre de fois que la boucle doit être exécutée ou une condition d’arrêt, si on ne sait pas à l’avance combien de fois une boucle doit être exécutée.

En Python, il existe deux sortes de boucles : « for » et « while ». Comme vous l’avez vu dans l’initiation à Python de ce site.

Sur cette page : http://www.wellho.net/mouth/2700_The-same-very-simple-program-in-many-different-programming-languages.html vous verrez comment sont écrites les boucles de type « for » dans de nombreux langages de programmation. Vous pourrez alors en comparer les syntaxes.

  • Structures conditionnelles

Dans l’initiation à Python, vous avez déjà vu les structures conditionnelles qui permettent de vérifier certaines conditions avant d’exécuter des morceaux de codes.

Ces structures reposent sur les instructions « if  (elif) else » et des opérateurs de conditions qui doivent renvoyer une valeur True (vrai) ou False (faux). On retrouve ces structures dans de nombreux langages avec des variantes dans la syntaxe.

  • Procédures et fonctions

Les procédures et fonctions permettent de centraliser des bouts de codes que l’on va exécuter souvent sous un nom qui permettra un appel en une seule instruction. Cela permet de n’écrire le code qu’une seule fois (et donc de limiter les erreurs de syntaxe). En plaçant ce code dans des bibliothèques, on pourra y faire appel dans plusieurs programmes (voir plus loin).

Procédures et fonctions doivent porter des noms (sans espaces) et peuvent recevoir des paramètres pour passer des variables afin d’y effectuer des opérations en fonction de celles-ci.

La différence entre les fonctions et les procédures vient du résultat attendu. Une fonction doit renvoyer un résultat alors qu’une procédure va exécuter des instructions sans renvoyer de résultat.

Voici un exemple en python :

Fonction de chargement d’un jeu et procédure de sauvegarde
def charge(nom):
   fichier = open(nom, "r")
   chaine = fichier.read()
   fichier.close()
   print(chaine)
   lignes = chaine.split("\n")
   jeu = [[caractere for caractere in ligne] for ligne in lignes]
   del jeu[-1]
   return jeu 

def sauvegarde(nom):
   lignes = ["".join(ligne) for ligne in jeu]
   chaine = "\n".join(lignes)
   fichier = open(nom, "w")
   fichier.write(chaine)

Résultats

L’appel de la fonction « charge » retournera le « jeu » (un tableau de positions pour un jeu d’échecs par exemple) alors que l’appel à la procédure « sauvegarde » ne retournera aucun résultat visible.

  • Objets

Les langages de programmation orientés objet (POO), comme Python, C++ ou Java permettent la création d’une structure particulière appelée « objet ».

La maîtrise de la programmation-objet n’est pas au programme en première, mais elle revient si souvent dans les langages de programmation moderne qu’il est important d’en avoir quelques notions.

Un objet appartient à une classe qui possède :

  • Un nom
  • Des attributs : qui sont des « valeurs » associées à cette classe
  • Des méthodes : qui sont des ‘actions’ que pourra faire l’objet (des fonctions et des procédures en fait)

Nous pourrions par exemple définir une classe « Fenetre » qui pourrait correspondre à une fenêtre graphique à l’écran. Cette Fenêtre devrait avoir un point d’origine (avec ses coordonnées x et y) ainsi que ses dimensions (dx, dy). Il faut pouvoir afficher cette fenêtre, la cacher ou encore la déplacer.

Notre classe « Fenetre » serait donc définie ainsi :


Nom
Fenetre
Attributsx : origine en x
y : origine en y
dx : largeur
dy : hauteur
MéthodesAfficher()
Cacher()
Deplacer()

Une fois la classe « Fenetre » définie, il sera possible de créer un ou plusieurs objets « Fenetre » dans le programme. On appelle ces objets des instances de la classe. La création de ces objets se fait simplement en appelant la classe et en définissant les attributs de départ. Ainsi, un objet « Fenetre_principale » pourrait être créé de la façon suivante :

  • Fenetre_principal = Fenetre(0, 0, 800, 600)

Ce qui aurait pour effet de créer une fenêtre d’une largeur de 800 pixels par 600 pixels à la position 0,0 de l’écran.

Cette fenêtre ne serait pas encore visible à l’écran. Pour la rendre visible, il faudrait appeler sa méthode « Afficher() » de la façon suivante :

  • Afficher()

Pour passer à un exemple plus concret, imaginons que nous voulions écrire des calculs de fractions en les conservant sous la forme « n/d » (où n est le numérateur et d le dénominateur).

Voici ce que cela peut donner en Python :

Programme de calcul de fraction avec un objet « Fraction »
""" Exemple Programmation Objet """

class Fraction:
    """ Constructeur pour initialiser les données d'une fraction. """

    def __init__(self, numerateur, denominateur):
        # deux tirets de chaque côté pour cacher la fonction
        self.n = numerateur
        self.d = denominateur
        """ l'utilisateur ne pourra pas modifier ces deux données : variables locales et privées """

    def numerateur(self):
        return self.n

    def denominateur(self):
        return self.d

    def __str__(self):
        return "%s/%s" % (self.n, self.d)

    def __mul__(self, f):
        return Fraction(self.numerateur() * f.numerateur(), self.denominateur() * f.denominateur())


""" Exemple de valeurs du type précédent """
f1 = Fraction(1, 2)  # exécute Fraction.__init__(1,2)
f2 = Fraction(1, 3)
print(f2 * f1)

Résultat

1/6

Dans ce programme, « Fraction » est un objet qui possède deux attributs : numérateur et dénominateur (sans accent dans le code). Il possède deux méthodes :

  • __str__ : où l’on utilise une conversion cachée en texte pour afficher la fraction sous la forme n/d.
  • __mul__ : qui permet de redéfinir l’opération de multiplication, sachant que nous n’avons pas le droit d’utiliser le caractère étoile pour cela (*).

2.    Diversité et unité des langages de programmation

Algorigrammes et Pseudo-code

Pour écrire un programme informatique, il faut déjà avoir une vision claire de ce que le programme doit accomplir et des différentes étapes nécessaires pour arriver à ce résultat. La définition de ces différentes étapes est appelée « algorithme ».

Avant de passer au code dans le langage choisi, il est important de spécifier ces étapes logiques en les rédigeant en français avec des phrases courtes et précises.

Exemple : pour calculer le périmètre d’un cercle à partir de son rayon

  • Demander la valeur du rayon
  • Calculer le périmètre avec la formule 2 × Pi × rayon
  • Renvoyer la valeur du périmètre

Ce programme peut ensuite être exprimé sous la forme d’un algorithme en utilisant un pseudo-code ou un algorigramme.

  • Pseudo-code

Le pseudo-code se rapproche de l’écriture en français, en utilisant une formulation plus structurée et proche de ce qui sera ensuite écrit dans le langage de programmation choisi.

Il comprend :

  • Un en-tête avec le nom de l’algorithme, une description, les données qui seront fournies en entrée du programme et le résultat attendu (avec leurs types).
  • Une liste des variables, constantes et autres objets utilisés.
  • La suite d’instructions nécessaires, délimitées par les mots « début » et « fin ».

Dans notre exemple précédent pour le périmètre, cela donne :

Pseudo-code pour calculer le périmètre d’un cercle
fonction Périmètre

Calculer le périmètre d’un cercle en fonction de son rayon

Paramètre :
	Rayon : réel
Résultat :
	Périmètre : réel

Pi = 3,14
Périmètre : réel

début
	Périmètre <- rayon × 2 × Pi
	Retourner Périmètre
fin

Attention, dans le programme final il faudra éviter l’utilisation d’accents et remplacer le signe multiplier par l’étoile « * ».

Voici la liste de mots-clefs qu’il faut utiliser pour l’écriture d’un pseudo-code :

Mots et symboles du pseudo-codeOpérations réalisées
DébutDébut de l’algorithme, permet de le nommer
FinFin de l’algorithme
FaireExécution d’une opération
EntrerAcquisition ou chargement d’une donnée
SortirÉdition ou sauvegarde d’un résultat
RetournerRetourner le résultat d’une fonction
¬Affectation d’une valeur à une variable
Symboles d’opérateur (+-*/ ET OU,…)Opérations arithmétiques ou logiques
Aller àBranchement inconditionnel (déconseillé́)
Si…alors…[sinon]Branchement inconditionnel
Selon cas…[autrement]Branchement conditionnel généralisé́
Tant que…faire…} Répétition conditionnelle
Répéter…jusqu’à…
Pour…=…à…Répétition contrôlée
  • Algorigramme

L’algorigramme représente l’algorithme sous la forme d’un graphique avec des formes codifiées et normalisées :

L’algorigramme du calcul de périmètre précédent serait :

On voit rapidement que l’inconvénient des algorigrammes est l’espace nécessaire pour les tracer. En revanche, ils sont parfois utiles, en particulier lors de l’utilisation de conditions, pour visualiser plus rapidement le déroulement de l’algorithme.

Langages par blocs

Les langages de programmation par bloc, comme MakeCode, Scratch ou Snap! Sont généralement destinés aux « débutants » programmation, ils font appel à une représentation graphique des éléments de codes sous forme de blocs qui s’imbriquent et permettent de créer facilement un programme sans être gêné par la syntaxe d’un langage de programmation plus évolué.

Exemple avec Snap! :

Ces langages s’approchent ainsi beaucoup du pseudo-code et certains permettent de créer ses propres blocs et de créer ainsi des programmes très évolués.

Notez également que Scratch et Snap!, qui ont été développés par des chercheurs de l’université de Berkeley, permettent de travailler sur des personnages et des scènes qui possèdent des attributs et des méthodes : ce sont donc des langages orientés objet.

Langages compilés ou interprétés

Les langages de programmation peuvent également être subdivisés en deux catégories suivant que leur code est transformé pour être compréhensible par le processeur au moment de l’exécution ou au moment de la conception.

Les langages comme le C, le C++ ou le Pascal sont des langages compilés. Une fois que vous avez fait votre programme, un compilateur (intégré à votre environnement de développement en général) va transformer ce code en une version de bas niveau qui sera compréhensible par le système sur lequel ce code va s’exécuter. Dans ce cas, la compilation ayant été faite par le programmeur, le programme démarre et s’exécute généralement plus rapidement. Le principal inconvénient vient du fait que cette compilation doit être faite pour un environnement particulier : un programme compilé pour Windows ne pourra pas s’exécuter sous Linux.

Pour pallier ce problème, un langage peut être interprété : il ne sera alors pas compilé, mais devra s’exécuter au sein d’un environnement nommé « interpréteur ». C’est le cas de Java : un programme Java ne peut fonctionner que si son interpréteur, la Java Virtual Machine (jvm) est installée sur le système. Cet interpréteur va alors compiler le code et l’exécuter pour un fonctionnement local. Dans ce cas, s’il existe des interpréteurs pour plusieurs environnements, un même code pourra s’exécuter dans ceux-ci. Ainsi un même programme en Java peut s’exécuter sous Windows, Mac ou Android à condition que le système cible dispose de la Java Virtual Machine adaptée.

L’inconvénient d’un interpréteur est donc qu’il faut qu’il soit installé sur le système, ce qui nécessite un téléchargement et une installation spécifique par l’utilisateur final. L’exécution est également généralement moins rapide, car le programme doit être « compilé » (en pratique le code Java est déjà précompilé dans un langage appelé bytecode) avant d’être exécuté.

Java est un langage très populaire, car il est entièrement orienté objet et il existe des machines virtuelles pour pratiquement tous les systèmes, y compris des systèmes embarqués (compteurs de gaz, lecteur médias…).

3.    Spécifications

Concevoir un programme

Un programme informatique est en général constitué d’une grande quantité de blocs de codes correspondants à différentes situations en fonction de ce que fait l’utilisateur.

Un jeu de dames sera par exemple constitué des éléments suivants :

  • Création et affichage du plateau de jeu
  • Mise en place des pièces
  • Gestion des actions de chaque joueur :
    • Lui demander de choisir une pièce qui peut bouger
    • Proposer une nouvelle position pour cette pièce
    • Effectuer le déplacement et rafraîchir le plateau de jeu en supprimant les pièces prises
  • Vérifier si un pion est devenu une dame
  • Afficher le score et vérifier si l’un des joueurs a gagné

Dans cet exemple nous n’avons même pas évoqué la création d’un « joueur » programmé et de son éventuelle programmation d’apprentissage automatisée.

Vous voyez que plusieurs de ces étapes, comme l’affichage ou le rafraîchissement du plateau, vont se produire à plusieurs reprises au cours du jeu. Afin de simplifier le code et de le rendre plus modulaire, il convient donc de le découper en plusieurs fonctions ou procédures.

Ce découpage permettra également de partager le travail si l’on constitue une équipe de développement : chaque membre de l’équipe sera chargé de développer certaines fonctions et procédures spécifiques.

Le programme sera donc découpé de la façon suivante :

  • Les définitions et les codes pour les procédures et les fonctions
  • Le corps du programme qui va faire appel à ces procédures et fonctions

Les commentaires

Il n’est pas rare que plusieurs personnes travaillent sur un même programme, ou que vous ayez besoin de reprendre un programme (ou un bout de code) plusieurs mois ou années après l’avoir écrit. Il est alors fastidieux d’avoir à se plonger dans le code pour essayer de comprendre ce que fait vraiment le programme.

Pour simplifier la tâche, il est possible et même souhaitable, de mettre des commentaires pour expliquer ce que vous faites.

Exemple : voici un code simple avec une fonction qui effectue le calcul de la factorielle (multiplication des nombres de 1 à la valeur. Factorielle (3) = 1 × 2 × 3 = 6)

Code Python pour le calcul de la factorielle
def fact(n):
    x = 1
    for i in range(2, n + 1):
        x *= i
    return x

print(fact(5))

Sans aucun commentaire, même ce code simple nécessite un peu d’attention pour comprendre ce qui se passe.

Il y aura deux façons de mettre des commentaires dans le programme :

  • À la suite du caractère dièse « # » : on peut alors mettre ce commentaire sur la même ligne (après le code) ou sur une seule ligne
  • entre deux séries de trois apostrophes « “””commentaire “”” » : il est alors possible de mettre ce commentaire sur plusieurs lignes.

Voici ce que cela peut donner dans le cas de l’exemple précédent :

Code Python pour la factorielle avec commentaires
"""
Définition de la fonction factorielle, nommée fact(n)
"""
def fact(n): #fonction de calcul de la factorielle
    x = 1 # on commence par définir le résultat à la valeur 1
    # boucle for, on va multiplier par les nombres successifs
    for i in range(2, n + 1): # inutile de multiplier par 1
        x *= i                # la boucle s'arrête à n
    return x # nous renvoyons la valeur finale

"""
Début du programme principal
"""
print(fact(5)) # affiche le résultat de la factorielle de 5

Les fonctions et docstring

Une procédure ne renvoie aucune valeur au programme, elle ne fait qu’exécuter des bouts de codes en fonction des arguments qu’on lui envoie et évite ainsi de saisir plusieurs fois des instructions que l’on utilise souvent. On peut les voir comme des sous-programmes et il est souvent possible de les tester indépendamment du programme principal.

Les fonctions doivent, en revanche, renvoyer un résultat et le reste du programme peut dépendre de celui-ci. Si le résultat est incohérent, ou si la fonction bloque le programme, cela impactera la suite de son fonctionnement.

Comme il n’est pas toujours évident de trouver d’où vient la faille, surtout si vous utilisez beaucoup de fonctions, il convient de porter une attention particulière à la définition de celles-ci.

Une première étape est de bien documenter la fonction en utilisant des commentaires juste après la déclaration de fonction pour indiquer ce que cette fonction va effectuer. C’est ce qu’on appelle le docstring en Python.

Par exemple :

Documentation d’une fonction avec docstring et affichage de celui-ci
def fact(n):
    """
    Cette fonction va renvoyer la factorielle du nombre entier n donné en argument

    :param n: (int) :: entier dont on va faire la factorielle
    :return:  (int)  :: entier résultat du calcul
    """
    x = 1
    for i in range(2, n + 1):
        x *= i
    return x

print(fact.__doc__)

Résultat de l’exécution du code

Cette fonction va renvoyer la factorielle du nombre entier n donné en argument

    :param n: (int) :: entier dont on va faire la factorielle    
    :return:  (int)  :: entier résultat du calcul

On voit dans cet exemple que le docstring peut être appelé et affiché dans un programme en donnant le nom de la fonction suivi d’un point et de la commande « __doc__ » (double souligné avant et après ‘doc’).

Ce docstring se compose de trois parties qu’il faut bien renseigner :

  • Une description précise de ce que fait la fonction.
  • Les types et natures de tous les arguments qui doivent être passés à la fonction.
  • Le type et la nature du résultat renvoyé par la fonction.

Dans PyCharm, le simple fait de mettre les trois guillemets de commentaires à la suite de la déclaration de fonction « def nomfonction(arguments) : » va pré compléter la liste des paramètres et le « return ».

Notez qu’il existe deux possibilités pour décrire les arguments. Celle de l’exemple ci-dessus où l’on donne le type entre parenthèses et ensuite la description (séparée ou pas par une ponctuation) ou l’utilisation des arguments « type ». On aurait donc pu écrire :

:param n: entier dont on va faire la factorielle
:type n: int

Cette docstring pourra également être appelée depuis la console python en important une bibliothèque (fichier contenant des fonctions) et en utilisant la fonction « help(nomfonction) ». Si vous voulez pouvoir réutiliser votre code efficacement, soyez précis et complet dans vos docstrings !

Notez que les docstrings peuvent (devraient !) être placés également au début des fichiers de bibliothèque (contenant des fonctions), au début du programme (pour dire ce qu’il fait), en description des classes d’objets et de leurs méthodes, pour les procédures…

Préconditions et postconditions

En dehors des fautes de frappe ou de syntaxe (qu’un bon environnement de développement pourra vous permettre de réduire significativement), plusieurs types d’erreurs peuvent survenir dans l’exécution d’une fonction :

  • Le type des arguments envoyés à la fonction peut ne pas correspondre à ce que le développeur a prévu
  • Le résultat du calcul ne correspond pas à ce qu’on attendait
  • Certaines valeurs précises empêchent d’arriver au résultat attendu : problème de précision des nombres flottants, calculs avec zéro, infini…
  • L’environnement a été mal défini et le calcul ne peut pas se faire, car on a atteint la limite du nombre de récursivités autorisées

Certains de ces problèmes peuvent être résolus en établissant clairement les préconditions et postconditions d’une fonction :

  • Préconditions: vérification des conditions avant le début de l’exécution de la fonction pour vérifier que les résultats seront corrects. Par exemple la factorielle de l’exemple précédent ne pourra être faite que si le nombre envoyé est un entier strictement positif.
  • Postconditions: vérification à la fin du calcul que celui-ci renvoi la bonne valeur (celle attendue)

Dans le code donné précédemment, les lignes :

:param n: (int) :: entier dont on va faire la factorielle
:return:  (int)  :: entier résultat du calcul

Sont donc déjà les préconditions et les postconditions de base de la fonction factorielle. Il est toutefois possible d’aller plus loin en s’assurant que les arguments donnés en entrée lors de l’appel de la fonction seront justes et de faire en sorte que le programmeur ait une identification claire de la nature du problème si ce n’est pas le cas. On utilise pour cela la commande « assert ».

« assert » sera suivi des conditions qui feraient échouer la fonction, ainsi que d’un message destiné au développeur pour indiquer la source d’erreur.

Par exemple dans la fonction factorielle on pourrait inclure les indications suivantes :

Fonction factorielle avec vérification des préconditions avec assert
def fact(n):
    """
    Cette fonction va renvoyer la factorielle du nombre entier n donné en argument

    :param n: (int) :: entier dont on va faire la factorielle
    :return:  (int)  :: entier résultat du calcul
    """
    assert n > 0, "attention : nombre négatif ou nul"
    assert type(n) == int, "attention, n pas entier"

    x = 1
    for i in range(2, n + 1):
        x *= i

    assert type(x) == int, "le résultat n'est pas entier"
    return x


print(fact(3))

Avec le code ainsi écrit, on vérifie que n est bien positif ou nul et on vérifie le type de x avant de renvoyer la réponse afin de générer une erreur avec un message clair.

Une possibilité alternative est l’utilisation des commandes « try: » et « except: ». Les instructions placées sous le bloc  « try: » sont exécutées si elles ne génèrent aucune erreur, sinon ce sont les instructions du bloc « except: » qui sont exécutées.

Voici un exemple :

Gestion des erreurs Python avec try et except
try:
    print(fact(1.5))
except:
    print("un problème est survenu")

Ici le programme va afficher « un problème est survenu », car il n’est pas possible de faire le factoriel d’un nombre non entier.

L’utilisation de assert ou de try/except est appelée de la programmation défensive. On peut éventuellement la compléter avec des conditions « if (elif) else » pour gérer automatiquement les erreurs, les corriger ou inviter l’utilisateur à entrer des valeurs correctes.

4.    Mise au point de programmes

Jeux de tests pour un programme

La programmation défensive devrait être complétée par des jeux de tests intégrés dans le programme. Deux types de jeux de tests sont possibles : l’utilisation de assert ou l’intégration d’instructions qui seront vérifiées par le module doctest de Python.

  • Avec assert

L’utilisation d’assert en tant que test dans le programme consiste à placer des instructions vérifiant des valeurs attendues. Par exemple, on pourrait tester une fonction qui vérifie si une année est bissextile ou pas en insérant une série d’années à des fins de test :

Tests avec assert pour une fonction bissextile
def bissextile(annee):
    if annee%4 == 0:
        return True
    else:
        return False

assert bissextile(2000) == True
assert bissextile(1900) == False

L’exécution du code ci-dessus va générer une erreur, car si on prend juste en compte le fait que l’année soit divisible par 4 (le symbole « % », appelé modulo, donne le reste de la division entière), 1900 sera considéré comme une année bissextile. Or une année bissextile ne doit pas être divisible par 100, ou alors être divisible par 400.

L’intégration de ce test permet donc de se rendre compte que la fonction est mal écrite.

Voici la version correcte qui passe les deux tests :

Fonction bissextile correcte
def bisextile(annee):
    if (annee%4 == 0 and annee%100 != 0) or annee%400==0:
        return True
    else:
        return False

assert bisextile(2000) == True
assert bisextile(1900) == False

Cette fois il n’y aura pas de message d’erreur.

  • Avec doctest

« doctest » est un module de Python qui permet une vérification des fonctions avec de très nombreuses options. Il va s’utiliser en plaçant dans le docstring de la fonction des exemples de l’exécution de celle-ci avec des valeurs précises ainsi que le résultat que l’on attend de la fonction dans ces cas-là.

Chaque appel test de la fonction est précédé de trois signes supérieurs : « >>> » et le résultat attendu est placé sur la ligne suivante. Ces appels seront placés dans une section nommée « :example: » de la docstring.

En fin de programme on va placer un code particulier qui va lancer la vérification de ces tests pour toutes les fonctions du programme. Ce code se présente de la façon suivante :

if __name__ == “__main__”:

import doctest

doctest.testmod()

Ce code ne va s’exécuter que si on exécute directement le fichier python qui le contient et pas si ce fichier est appelé depuis un autre fichier. C’est intéressant pour tester des modules indépendamment les uns des autres sans faire apparaître ce test si on lance le programme principal ne contenant pas ce code.

Sachant qu’un programme est généralement composé d’un programme principal faisant appel à des modules contenant diverses fonctions (voir les bibliothèques plus bas).

Voici ce que donne le programme d’année bissextile précédente avec doctest :

Programme utilisant doctest
def bissextile(annee):
    """
    Fonction qui détermine si une année est bissextile

    :param annee: (int) année à tester
    :return: Vrai si l'année est bissextile, sinon Faux

    :Example:
    >>> bissextile(2000)
    True
    >>> bissextile(1900)
    False
    """
    if (annee%4 == 0 and annee%100 != 0) or annee%400==0:
        return True
    else:
        return False

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)

Résultat de l’exécution

Trying:
  bissextile(2000)
Expecting:
  True
ok
Trying:
  bissextile(1900)
Expecting:
  False
ok
2 passed and 0 failed.
Test passed.

L’ajout de “verbose=True” permet d’avoir un retour des tests. Si on omet cet argument, doctest ne renvoie aucun message en cas de test réussi.

L’utilisation de jeux de tests pour un programme permet donc de réduire significativement les erreurs de programmation les plus courantes et vous permettra de gagner du temps. En plus de la « certification » formelle produite par les tests, la réflexion nécessaire à leur mise en place vous obligera à prendre en compte toutes les situations possibles pour rendre vos programmes plus robustes et fiables.

Les indications pour doctest vont aussi apparaître lorsqu’on effectuera l’affiche du docstring d’une fonction et donnera ainsi à un utilisateur tiers de vos fonctions une idée des résultats auxquels il doit s’attendre.

Gérer le travail en équipe pour la conception

Lorsque vous aurez à développer un logiciel informatique en équipe, vous devrez mettre en place des procédures de gestions de projet afin de parvenir à tenir les délais et à obtenir un programme fonctionnel.

  • Cahier des charges

La première étape va consister à rédiger un cahier des charges clair de ce que doit faire votre logiciel. Soyez le plus précis possible et essayez de prendre en compte toutes les demandes de votre « client ».

Par exemple, si vous devez développer un jeu d’échec :

  • Le jeu est-il destiné à deux joueurs ou à un joueur seul contre l’ordinateur ?
  • Les tours sont-ils en temps limités ?
  • faut-il prévoir plusieurs niveaux de difficulté ?
  • Y’a-t-il plusieurs variantes des règles du jeu
  • Sur quel type de système le jeu doit-il fonctionner ?
  • Allez-vous enregistrer les scores et les afficher ?
  • Une partie sera-t-elle possible en ligne ? Si oui de quelle manière ?

Une fois que ces étapes seront claires, vous devrez partager le travail dans votre équipe.

  • Qui fait quoi et dans quel délais

En fonction des affinités et des compétences des membres de l’équipe, chacun va devoir prendre en charge une partie de la programmation. Il est important que l’un des membres de l’équipe soit désigné comme « chef de projet » : c’est lui qui va vérifier que tout se passe bien et maintenir la cohérence du projet.

La planification du projet peut passer par un diagramme de Gantt (https://www.gantt.com/fr/ ) ou d’autre outils similaires, l’important étant de prévoir dans quel ordre les parties du programme seront développées.

Chaque membre de l’équipe va alors travailler sur un bout de code et le chef de projet sera chargé d’intégrer ces bouts de codes au programme principal.

Pour rendre les choses plus simples à gérer, il est conseillé que chaque développeur du projet gère son propre fichier de code contenant les procédures et fonctions qu’il doit programmer. Ces fichiers seront des bibliothèques de codes utilisées par le programme principal.

Cet organigramme « idéale » n’est toutefois pas toujours possible et il faut souvent que plusieurs personnes travaillent sur le même fichier. Pour éviter d’effacer le travail des autres et pour le prendre en compte efficacement il faut donc utiliser un outil spécifiquement destiné au travail de programmation par équipe avec contrôle de version : Git

  • Utiliser Git pour développer en équipe

Git est un système de contrôle de version open source décentralisé développé en 2005 par Linus Torvalds, le créateur de Linux. Il est très vite devenu une norme dans l’industrie informatique et aucun projet logiciel sérieux ne se fait aujourd’hui sans utiliser Git.

Git peut être installé gratuitement sur n’importe quel serveur (il est open source), mais il est probable que vous n’ayez pas votre propre serveur disponible 24h sur 24 avec une bonne connexion Internet. Vous allez donc utiliser les services gratuits de GitHub, comme plus de 100 millions de projets de développements dans le monde.

La première chose à faire est de se rendre sur le site de Github (https://github.com/ ) et d’y créer un compte pour chaque développeur de votre projet.

Le chef de projet va ensuite créer un « repository », qui est un dossier dans lequel seront conservés tous les fichiers du projet avec leurs versions successives. Au moment de la création, mettez-vous d’accord avec l’équipe pour savoir si le projet sera géré de manière publique (tout le monde peut voir où en est votre projet) ou privée. Pensez à cliquer la case « Initialize this repository with a README » pour créer le début de la structure et vous simplifier la tâche par la suite.

Dans l’onglet « Projects » de votre « repository », le chef de projet va créer le « projet » associé à votre dossier afin de gérer les tâches durant le développement. Cette procédure est optionnelle, mais sera très utile pour la gestion de gros projets. Pensez à prendre un « template » (Basic Kanban est un bon point de départ) afin de gagner du temps par la suite.

La partie « Projects » sera composée de trois volets : To Do (choses à faire), In Progress (en cours de développement) et Done (tâches terminées). Vous pourrez alors créer des tâches dans « To Do » et les glisser dans les autres volets au fur et à mesure du développement.

Enfin, le chef de projet se rendra dans l’onglet « Settings », puis « Manage access » pour y inviter les membres de l’équipe avec le bouton « Invite a collaborator » en indiquant leur pseudo GitHub.

  • Intégration de GitHub dans PyCharm

Dans les préférences de PyCharm, vous allez maintenant pouvoir connecter votre compte GitHub en indiquant vos identifiants dans la bonne fenêtre, comme ci-dessous.

Vous allez dans la partie « Version Control », puis « GitHub » et vous cliquez sur le petit bouton « » en bas à gauche de la grande case vide. Vous renseignez alors le Login et le Password et cliquez sur « Log In ».

Si tout s’est bien passé, vous devriez voir apparaitre votre avatar GitHub suivi de votre pseudo :

Dans le menu « VCS » de PyCharm vous allez ensuite cliquer sur « Get from Version Control… » puis choisir votre projet dans la fenêtre qui va s’ouvrir (dans l’onglet « GitHub ». Ouvrez alors ce projet dans une nouvelle fenêtre (si vous avez déjà une fenêtre ouverte) pour obtenir un écran qui ressemble à :

En bas à droite de votre fenêtre va apparaître un bouton portant le nom de la branche dans laquelle vous allez travailler (master par défaut).

  • Méthode de développement avec GitHub

GitHub fonctionne par « branches » de projet. La branche « master » est la branche par défaut dans laquelle devrait apparaître la dernière version fonctionnelle du projet. Les autres branches, que vous allez créer, permettent de faire des développement en parallèle de différentes fonctionnalités en partant d’un état du « master ».

Pour une gestion de projet efficace, évitez de créer de nouvelles fonctionnalités directement dans votre « branche » master !

Chaque développeur devrait commencer par créer une branche avec un nom explicite pour y développer de nouvelles fonctionnalités. Une fois que celles-ci sont testées et fonctionnelles, le chef de projet va les intégrer au « master ».

Pour prendre en compte le travail fait par les autres développeurs, commencez toujours par lancer la commande « Pull » (« update project », flèche bleue dans PyCharm) pour récupérer la dernière version du projet.

A chaque modification importante, faites un « Commit » (coche verte dans PyCharm) et mettez un commentaire pour dire ce que vous avez fait. Cela va permettre de revenir en arrière en cas de problème dans le code.

Quand vous avez fait des modifications nombreuses ou que vous terminez votre session, effectuez un « Pull » pour récupérer ce que d’autres auraient pu faire pendant que vous avez travaillé, suivi d’un « Push » pour partager votre travail sur GitHub.

Pensez à toujours faire un « Pull » avant de lancer « Push » !!

Il se peut qu’au moment du « Pull » vous ayez à gérer les conflits de versions et à décider de quelle version vous allez conserver. Si chacun travaille dans sa propre branche, la gestion des conflits ne va apparaître que lors de la fusion (merge) avec la branche principale et sera gérée par le chef de projet :

La fenêtre « merge » indique la branche dans laquelle vous allez intégrer les codes (à gauche), celle d’où vient le code « nouveau » (à droite) et le résultat final souhaité (au centre). Les flèches permettent alors de sélectionner ce que vous voulez conserver de chacune des branches.

Au fur et à mesure du déroulement de votre projet, vous allez pouvoir intégrer les éléments de chaque développeur dans le programme principal :

GitHub vous permet de voir où vous en êtes dans le développement, quelles modifications ont été faites par qui et quand et vous pourrez toujours revenir en arrière.

5.    Utilisation de bibliothèques

Créer une bibliothèque de fonctions

Nous avons vu que le développement d’un logiciel en équipe est plus efficace si chaque développeur programme ses propres fonctions. Une fois que ces fonctions ont été testées et qu’elles se déroulent correctement, il faut pouvoir les utiliser à nouveau dans d’autres projets.

Pour cela, vous pouvez les regrouper par thème dans un fichier que vous allez réutiliser dans vos autres projets en créant une « bibliothèque ». Celle-ci contient des fonctions et des procédures aux noms explicites et bien documentés avec des docstring.

Créer votre propre bibliothèque, il suffit simplement de rajouter un docstring en tête de votre fichier python pour expliquer ce que contient votre bibliothèque et de sauvegarder le fichier avec une extension  « .py ».

Le docstring devrait contenir les informations suivantes :

  • Une description de ce que fait cette bibliothèque.
  • Une liste des fonctions et procédures classées par famille. Ne rentrez pas dans le détail de chacune d’entre elles puisqu’elles possèdent leur propre docstring.
  • Un numéro de version, avec la date de création et un contact si vous distribuez cette bibliothèque en ligne. Ajoutez une licence d’utilisation pour faciliter sa réutilisation par d’autres développeurs.

Utiliser la documentation d’une bibliothèque existante

Pour savoir ce que contient une bibliothèque de fonction et comment les utiliser (quels arguments fournir, quel résultat attendre), vous pouvez utiliser la console Python et charger la bibliothèque avec la commande « import » avant de lire son contenu avec la commande « help(nom) ».

Par exemple, les commandes suivantes permettent de lire les docstring de la bibliothèque math de Python :

>>> import math
>>> help(math)

Intégrer une bibliothèque à un programme Python

Il y a plusieurs manière d’importer une bibliothèque dans un programme Python, mais dans tous les cas il faut que le fichier de cette bibliothèque soit accessible au programme et donc dans le même répertoire ou dans un répertoire enregistré dans le chemin d’accès de votre environnement de développement.

Pour importer l’intégralité des fonctions d’une bibliothèque, vous ajouterez la commande « import nom_bibliotheque » au début de votre code. Chaque fonction sera alors appelée par la commande « nom_bibliotheque.fonction »

Exemple :

import math

cosangle = math.cos(1)

Si le nom de la bibliothèque vous semble trop long ou pas assez explicite, vous pouvez lui attribuer un alias dans votre code en utilisant le code « import bibliotheque as alias » :

Exemple :

import math as m

cosangle = m.cos(1)

Il est aussi possible d’importer uniquement quelques fonctions précises d’une bibliothèque, sans tout utiliser. Dans ce cas on aura pas besoin de mettre le nom de la bibliothèque à chaque fois en utilisant le code « from bibliotheque import fonction » :

Exemple :

from math import cos

cosangle = cos(1)

Et vous pouvez importer toutes les fonctions de la bibliothèque avec la commande étoile « * » et les utiliser de la même façon que ci-dessus :

Exemple :

from math import *

cosangle = cos(1)

Chapitre précédent

Chapitre suivant