Chapitre 9 : Programmation et gestion des bugs

Science is what we understand well enough to explain to a computer. Art is everything else we do. (D. Knuth)


Remarque : Survoler les termes soulignés pour obtenir plus d'informations.

Notion de programme en tant que donnée

Introduction

Un programme, stocké sur un disque dur, SSD, ou transféré sur internet lorsqu'on le télécharge, est assimilable à de l'information : ainsi, un programme est une donnée numérique.

Le but d'un programme est de prendre une entrée (données, son, image, mouvements sur un écran tactile ou pad) et de produire une sortie, pour résoudre un problème donné (opérer un patient à distance, appliquer une transformation sur une image, divertir le joueur...).
C'est naturel : les humains ont souvent été attirés par la résolution de problèmes ; L'Histoire retient Al-khwarizmi (environ 800), algébriste, comme créateur du concept d'algorithme : un algorithme est une «suite d'instructions» ayant pour but de résoudre un problème donné. Il n'a donc pas «inventé» les algorithmes, qui existaient bien avant lui (algorithme d'Euclide en -300, recettes de cuisine, taille d'un silex, réplication de l'ADN par les ribosomes, ...) mais a commencé à poser des bases rigoureuses pour ce concept (algorithme de résolution des équations de degré 2).

Ce travail est poursuivi, suite à la crise des fondements en mathématiques et à leur axiomatisation, dans un contexte impulsé par David Hilbert et son programme sur les fondements des mathématiques (peut-on prouver que toute question mathématique a une réponse ?), par une formalisation de la notion par Alan Turing et par Alonzo Church .
La thèse de Church-Turing est essentiellement une définition du caractère algorithmique, dans un sens rigoureux et général :

Un problème qui peut être résolu algorithmiquement est dit «calculable» (dans le sens : il peut être résolu par une machine de Turing).

Les machines de Turing offrent un cadre théorique qui permet de répondre (négativement) au problème de décision : «pour tout problème donné, existe-t-il un algorithme qui permet de répondre en un nombre fini d'étapes, par «oui» ou «non» à ce problème» ; on verra qu'il n'existe pas de programme qui permet de voir si un programme donné s'arrête ou boucle infiniment.

Tout langage de programmation commun (Python, Java, C, Javascript) se comporte comme une machine de Turing (à la différence près que le ruban est fini) ; c'est même démontré mathématiquement en utilisant les instructions de bases communes à tous ces langages. Tout programme dans ces langages peut être codé sur une machine de Turing. Mais dans le sens de l'Histoire, ce sont les machines de Turing, avec l'aide de concepts inventés par Von Neumann, qui ont inspiré et rendus possibles les premiers véritables ordinateurs.

Calculabilité

Ce qui est calculable est ce qui peut être exécuté par une machine de Turing. Une machine de Turing est un modèle théorique imaginé par Alan Turing en 1936, pour modéliser, étudier et généraliser les premières machines s'approchant d'un ordinateur.

Une machine de Turing est contituée d' :

La machine de Turing s'arrête dans deux cas : Si la machine de Turing ne s'arrête pas, le mot de départ est refusé.

On a représenté la machine de Turing suivante, dans son état initial :
La tête de lecture est sur le "a" le plus à gauche (flèche orange). Côté automate, on part de l'état q1.
Première étape : on est sur "a", et une flèche sort de q1 en mentionnant la possibilité "a" : on va suivre cette flèche à droite de q1, passer en q2, et au passage, remplacer, comme mentionné par la flèche suivie, ce "a" sur le ruban par un vide "#", et déplacer la tête de lecture à droite (utiliser votre doigt).

Le mot "abbabb" est-il accepté ? Le mot "abbabb" n'est pas accepté : on s'arrête en q1 qui n'est pas un état final.
Qu'est devenu ce mot ? (état final du ruban) Le mot initial est devenu "#bbab#".
Le mot "aaabbb" est-il accepté ? Ce mot est accepté : on s'arrête sur l'état final q4.
Qu'est devenu ce mot ? L'état final du ruban est "########".

La machine de Turing permet de «programmer» des calculs. On va montrer que la fonction prédécesseur (pour les nombres entiers positifs) est calculable, ainsi que l'addition.
Pour cela, nous avons besoin de la représentation unaire des entiers. En unaire, l'entier x, noté x sera représenté par (x+1) caractères "1" côte à côte. Ainsi : 0 sera représenté en unaire par "1" et 4 sera représenté par "11111".

Pour la machine suivante, on part du mot q1"# x#", où x est un entier naturel, écrit en unaire sur le ruban (la tête de lecture, comme précisé par la notation, pointe sur le premier "#" à gauche).


Si x = 0, que se passe-t-il ? Le mot de départ est-il accepté ou réfusé ? On suit toutes les flèches vers la droite pour arriver à l'état final : 0 est accepté.
Que fait cette machine pour x ≥ 1 ? On obtient le mot "#1###...#", qui correspond à 0.
En déduire que la fonction nulle (c'est-à-dire la fonction qui quelque soit x renvoie 0) est calculable. On peut écrire n'importe quel entier sur le ruban, la machine renvoie 0 : elle calcule une fonction des entiers vers les entiers qui donne 0 partout (fonction nulle). La fonction nulle est donc bien calculable.
Écrire la table d'action de cette machine (sous forme de tableau).
état courantsymbole lusymbole écritdéplacementétat suivant
q1 # # q2
q2 1 1 q3
q3 1 # q3
# # q4
q4 # # q4
1 1 q5
q5 Arrêt
On part du mot q1"#x#y#", où x et y sont des entiers naturels, écrits en unaire sur le ruban (la tête de lecture, comme précisé par la notation, pointe sur le premier "#" à gauche).
Construire une machine de Turing permettant d'écrire "# x+y#" ; on prouve ainsi que l'addition des entiers est une opération calculable.
On donne la table d'action d'une machine de Turing et un ruban vide :
état courant symbole lu symbole écrit déplacement état suivant
q1 # 0 q2
q2 # 1 q1

Quel est le résultat ? La machine de Turing s'arrête-t-elle ?
Le résultat est "01010101010...", le développement binaire du nombre rationnel 1/3 (calculable). La machine ne s'arrête pas.

Remarque : On peut coder une ou plusieurs machines de Turing à l'intérieur d'une machine de Turing : on parle de machine de Turing universelle

Cf la vidéo Science4All - La machine de Turing | Intelligence Artificielle 4
Voir la vidéo CNRS - Le modèle Turing

Décidabilité

Langage : Les problèmes peuvent être codés dans un langage formel, écrits en utilisant un alphabet (fini) de caractères pouvant être manipulés par des machines de Turing.
Problème de décision : C'est un problème pouvant être traité par une machine de Turing, pour lequel on peut répondre par oui ou bien par non.
Par exemple :

  1. savoir si un nombre entier donné est pair ;
  2. savoir si un nombre entier est premier ;
  3. un voyageur se téléporte à chaque étape à des coordonnées GPS différentes. Est-il apparu à Toulouse avant la 100ème téléportation ?
  4. un voyageur se téléporte à chaque étape à des coordonnées GPS différentes. Est-il apparu à Toulouse à une étape donnée ?

Décidabilité : On dit qu'un problème est décidable lorsqu'il est calculable et qu'une machine de Turing permet de répondre "oui" par un état final ou bien "non" par un arrêt sur un autre état, c'est-à-dire répondre au problème en un nombre fini d'étapes. Concrètement, il existe un algorithme qui permet de répondre au problème et il s'arrête en donnant la réponse.
Dans le cas contraire, on dit que le problème est indécidable.
S'il existe un algorithme pouvant répondant "oui" en un nombre fini d'étapes, mais ne pouvant pas répondre "non" en un nombre fini d´étapes, on dit que le problème est semi-décidable.

Dans la liste de problèmes de décision qui précède, expliquer lesquels sont décidables ou indécidables.
  1. Décidable : il suffit de regarder le reste de la division par 2.
  2. Décidable : il suffit de tester les diviseurs éventuels inférieurs à l'entier donné (ou même mieux : inférieurs à sa racine carrée).
  3. Décidable : on aura le résultat à ou avant la 100ème étape.
  4. Indécidable : tant que le voyageur n'apparaît pas à Toulouse, on n'obtient pas de réponse. Une simulation éventuelle de cette situation peut ne jamais se terminer.

Problème de l'arrêt : Ce problème cherche à savoir s'il existe un programme (appelons-le halt()), qui prend en entrée n'importe quel programme x (donné sous forme de chaîne de caractères, ou bien codé sous forme de nombre, par exemple) et précise si x s'arrête ou non.
Trop beau pour être vrai ! On va voir que ce problème est indécidable, et qu'un tel programme halt ne peut exister.

Examinons l'algorithme suivant :


	TURING(n)   # n est un entier naturel

	1. LISTER (sans les exécuter) tous les programmes de taille inférieure ou égale à n caractères,
		qui prennent un entier en entrée et renvoient un entier en sortie.
		# c'est long, mais possible par reconnaissance de la syntaxe et de la grammaire 
		# de ces programmes ; aussi, ces programmes sont en nombre fini.

	2. SÉLECTIONNER parmi cette liste ceux qui s'arrêtent avec halt() lorsque leur entrée est n.

	3. EXÉCUTER chacun des programmes sélectionnés et choisir le plus grand des entiers Smax 
		apparu en sortie de ces programmes.
		
	4. RETOURNER Smax + 1.
	
Une fois cet algorithme programmé, notons n0 sa taille en caractères.
Notons que 1., 2. ,3. et 4., par construction, contiennent uniquement des étapes qui s' arrêtent : globalement, le programme TURING() s'arrête.
Exécutons TURING(n0) :
  1. TURING(n0) se liste lui-même puisque sa taille est inférieure ou égale à n0.
  2. l'entrée de TURING() étant n0, et celui-ci s'arrêtant par construction, il est sélectionné par halt.
  3. TURING(n0) renvoie un entier qui est forcément inférieur ou égal à Smax...
  4. ... alors qu'il devrait renvoyer Smax + 1... CONTRADICTION !
Le programme halt ne peut donc pas exister : sinon, on pourrait s'en servir pour écrire un programme qui renvoie une sortie plus grande que la sortie qu'il renvoie.
Cf la vidéo Centre H. Lebesgue

Paradigmes en informatique

Langages interprétés ou compilés

Pour exécuter un programme, on peut, encore aujourd'hui, produire un exécutable à partir d'un script où sont écrites, à l'aide d'une syntaxe minimale, une suite d' instructions (hardware) du processeur permettant d'agir directement sur les registres.
On appelle ce pseudo-langage l'assembleur ; il est propre à chaque processeur car leurs jeux d'instructions peuvent être différents. On le qualifie de langage de bas niveau, dans le sens où il est très proche du matériel ; le plus proche étant le langage machine, qui est essentiellement de l'assembleur écrit en binaire. Voici un exemple d'opération écrite en assembleur (ces instructions sont assez communes et ce script marcherait sur bon nombre de processeurs) :


;pour calculer (7+6)%3, on aura le code suivant : 
MOV AX, 7	; Le registre AX, reçoit la valeur 7.
ADD AX, 6	; On ajoute à AX la valeur 6, le résultat est stocké dans la destination, soit AX.
MOV BX, 3	; On met dans le registre BX la valeur 3 qui servira de source à la division.
DIV BX		; On divise AX par la source, ici BX, qui vaut 3
;le résultat de la division est stocké dans la destination (BX)
;et le modulo (reste de la division qui nous intéresse ici) est mis dans DX

Cf Wikiversity

Ce type de travail est amusant au départ, mais vite fastidieux si l'on veut développer des programmes un peu longs. C'est une enseignante en mathématiques, devenue ingénieure informatique dans la navy, Grace Hopper, qui développa le premier compilateur (A-0 System) suivis en 1959 du langage COBOL (elle a aussi participé au travaux de normalisation du Fortran). Elle défendait l'idée qu'un programme devrait être écrit en langage naturel plutôt qu'en langage machine.


Le rôle d'un compilateur est de transformer un code source, écrit dans un langage (plus naturel que l'assembleur) en un fichier binaire exécutable sur une architecture matérielle et un système déxploitation donné.

Il s'oppose à l'interpréteur, qui est un logiciel qui exécute directement le code source d'un langage (interprété).
Parmi les langages suivants, dire lesquels sont compilés, interprétés ?
Python, C, C++, C#, Arduino, Bash,Javascript, HTML+CSS, SQL, Haskell.
Compilés : C, C++, C#, Arduino, Haskell (compilateur GHC)
Interprétés : Python (par le shell Python), Bash (par le shell Bash ou autre), SQL (par le SGBD), HTML+CSS et Javascript (par le navigateur, qui dispose généralement aussi d'une console), Haskell (interpréteur GHCi).
Des positions un peu particulières peuvent s'observer : c'est le cas du langage Java, inventé par James Gosling .
Avec Java, le code source est compilé en un fichier .jar (bytecode) qui sera exécuté par la machine virtuelle Java adaptée au système d'exploitation sur lequel on exécute le programme. Les .jar sont donc portables sur tous les systèmes disposant d'une machine virtuelle du même type. Cette grande portabilité est une des raisons qui a poussé les concepteurs d'Android à baser leur système d'exploitation sur Java. Proposer un schéma du même type que les précédents pour le langage Java.

Niveau de langage :
  • Si on se rapproche du matériel (code assembleur), on dit que le langage est de bas niveau ;
  • Si on se rapproche d'un langage de type humain (en ajoutant des fonctionalités, ...), on dit que le langage est de haut niveau.

Paradigmes de programmation

Le terme paradigme signifie à la fois :

Nous avons déjà étudié la programmation impérative (et ses réalisations itératives et récursives) ainsi que la programmation orientée objet en début d' année.
Nous utilisons aussi parfois, en Python, le paradigme fonctionnel. Le principe est de composer (enchaîner) des fonctions. En ayant des fonctions rigoureusement spécifiées, on limite, voir on élimine les effets de bord, et donc bon nombre d'erreurs possibles, tout en rendant le code source concis et lisible.
Haskell est un bon exemple de langage de programmation fonctionnelle. Liens pour découvrir ce langage : nokomprendo et univ-lille1.fr.

Récupérer, lire, compiler et exécuter quelques codes Haskell (hello world, factorielle, Fibonacci, ...) ; on pourra utiliser le compilateur en ligne repl.it.
main = do
  putStrLn "Hello World !"

fac n = product [1..n]

fibs = 0 : 1 : (zipWith (+) fibs (tail fibs))
fib n = fibs !! n

Attention, l'idée du paradigme fonctionnel n'est pas seulement d'utiliser des fonctions mais d'utiliser des fonctions dites pures. Il s'agit de fonctions qui ne modifient pas l'état courant des variables. Autrement dit, le résultat renvoyé par une fonction pure ne doit dépendre que de valeurs passées en paramètres et pas de valeurs externes à la fonction. De plus une fonction pure ne doit pas modifier les valeurs des variables externes, globales.


La fonction suivante est-elle une fonction pure ? Sinon, modifier cette fonction pour la rendre pure.

def est_majeur(age):
	return age >= 18
			
Cette fonction est pure car sa réponse ne dépend que de son paramètre age. De plus on constate qu'elle ne modifie aucune variable externe.
La fonction suivante est-elle une fonction pure ? Sinon, modifier cette fonction pour la rendre pure.

liste = [1, 6, 4]
def ajouter(x):
    liste.append(x)
    return liste
		
Cette fonction n'est pas pure car sa réponse dépend d'une variable externe qui n'a pas été passée en paramètre : liste
Fonction pure :

def ajouter(liste, x):  # Cette fonction est pure
    liste = liste + [x]
    return liste
			
Attention, la fonction suivante n'est pas pure !

def ajouter(liste, x):  # Cette fonction n'est pas pure
    liste.append(x)
    return liste
	
En effet, en python, la méthode append va modifier la variable externe liste qui a été passée en paramètre de la fonction. Tandis que la première fonction créé une réelle copie de la variable liste au sein de son espace de variables, grâce à l'opérateur de concaténation, la deuxième fonction agit directement sur la variable externe liste ce qui correspond à un effet de bord.

Avec un même langage de programmation, on peut utiliser des paradigmes différents.
Dans un même programme, on peut utiliser des paradigmes différents.

Implémenter en python un programme qui demande à l'utilisateur d'entrer des nombres et en effectue la somme :
selon le paradigme de programmation impératif (instructions simples sans fonction, ni objet).

# Récupère la liste de nombres
liste = input("Entrez la liste de nombres séparés par un espace : \n").replace(",", ".").split()
somme = 0
for nb in liste:  # boucle sur la liste
    somme = somme + float(nb)
	
print(somme) # affiche la somme
	

selon le paradigme de programmation procédural (définition d'une procédure sous forme de fonction ou de module). L'avantage du paradigme procédural est la réutilisation facile du code, ici par l'appel d'une fonction, mais cela peut être aussi par l'utilisation de module.

def somme_liste(liste):  # définit la fonction (procédure) pour effectuer le calcul
    somme = 0
    for nb in liste:
        somme = somme + float(nb)
    return somme

# Récupère la liste de nombres
liste = input("Entrez la liste de nombres séparés par un espace : \n").replace(",", ".").split()
s = somme_liste(liste)  # récupère la somme
print(s)  # affiche la somme

		

selon le paradigme de programmation fonctionnel (aucune création, ni modification de variable). Le paradigme de programmation fonctionnel s'attache à ne pas créer ou modifier de variables pour éviter les effets de bord. Tout passe par les paramètres de fonctions.

def somme_liste(liste):  # définit une fonction pure récursive pour effectuer le calcul
    if len(liste) == 0:  # cas trivial
        return 0
    return float(liste[0]) + somme_liste(liste[1:])  # appel récursif pour effectuer la somme

# Récupère la liste de nombre, appele la somme et affiche le résultat.	
print(somme_liste(input("Entrez la liste de nombre : \n").replace(",", ".").split()))  
	

selon le paradigme de programmation orienté objet (utilisation d'un constructeur d'objet).

class Calculateur:  # Constructeur d'objet
    def __init__(self, liste):  # Initialisation de l'objet
        self.liste = liste
		
    def somme(self): # méthode de calcul de la somme à partir de sa liste
        somme = 0
        for nb in self.liste:
            somme = somme + float(nb)
        return somme

unCalcultateur = Calculateur(input("Entrez la liste de nombre : \n").replace(",", ".").split())  # Invocation d'un objet Calculateur
print(unCalcultateur.somme())  # Appel de la méthode somme() et affichage du résultat
	

Mise au point des programmes

Cette partie est la suite logique du chapitre sur les spécifications et les tests vu en première NSI.

The Zen of Python (PEP 20)
Exécuter la ligne de commande import this dans la console python pour re-découvrir les grands principes de python : The Zen of Python (PEP 20)
  1. Préfèrer le beau au laid,
  2. … l'explicite à l'implicite
  3. … le simple au complexe,
  4. … le complexe au compliqué,
  5. … le déroulé à l'imbriqué,
  6. … l'aéré au compact.
  7. La lisibilité compte.
  8. Les cas particuliers ne le sont jamais assez pour violer les règles,
  9. … même s'il faut privilégier la praticité à la pureté.
  10. Ne jamais passer les erreurs sous silence,
  11. … ou les faire taire explicitement.
  12. En cas d'ambiguïté, résister à la tentation de deviner.
  13. Il devrait y avoir une – et de préférence une seule – façon évidente de procéder,
  14. … même si cette façon n'est pas évidente à première vue, à moins d'être Hollandais.
  15. Mieux vaut maintenant que jamais,
  16. … même si jamais est souvent préférable à immédiatement.
  17. Si l'implémentation s'explique difficilement, c'est une mauvaise idée.
  18. Si l'implémentation s'explique facilement, c'est peut-être une bonne idée.
  19. Les espaces de noms sont une sacrée bonne idée, utilisons-les plus souvent !

Rappels sur les assertions

Rappel de la syntaxe python des assertions (1re) :


assert condition
assert condition, "message d'erreur"

L'assertion vérifie qu'une condition est respectée, sinon elle stoppe l'exécution du programme et peut afficher un message d'erreur (celui-ci est facultatif).
Une assertion permet de stopper préventivement une exécution avant qu'une erreur ne se produise. Elles sont des outils de développement et ne doivent pas apparaître dans un programme final, livrable. En supprimant toutes les assertions d'un script, le programme doit continuer à fonctionner normalement.
Le mode de programmation qui utilise des assertions est appelée programmation défensive. Elle suppose que les fonctions sont correctement utilisées, mais prévoit des garde-fous : les assertions, qui permettent de vérifier les préconditions et les postconditions et ainsi de détecter des bugs au cours du développement.

Ce type de programmation est utilisé pour du code dont on a le contrôle. Par exemple lorsqu'on écrit un module, pour les fonctions qui sont destinées à n'être appelée que dans ce module. Pour les fonctions destinées à être appelées par d'autres modules/scripts/personnes, il est préférable de faire une gestion active des erreurs avec une structure conditionnelle qui retourne des valeurs facilement interprétables et si possible de même type que autres retours.

Compléter/modifier le script suivant afin que la fonction ne renvoie pas d'erreur quand l'élément n'est pas dans la liste mais renvoie la valeur -1, à l'aide d'une structure conditionnelle.

def indice(liste, element):
    """Renvoie l'indice de la première occurence de l'élément dans la liste."""
    assert element in liste, "L'élément doit être dans la liste."
    return liste.index(element)

print(indice(["a", "b", "c", "d"], "e"))
		

def indice(liste, element):
    """Renvoie l'indice de la première occurence de l'élément dans la liste. Renvoie -1 si l'élément n'est pas dans la liste."""
    if element not in liste:
        return -1
    return liste.index(element)
		
Ce comportement, qui renvoie -1 quand l'élément n'est pas dans la liste, est celui de la méthode liste.indexOf(element) en javascript.

Modularité

Définition

La modularité est le fait de décomposer un programme en plusieurs fonctions/modules/fichiers.

La modularité présente des avantages d'autant plus fort qu'un projet est important :

Cette modularité existe à plusieurs échelles :

Un module constitue une "brique" logicielle d'un projet, pouvant éventuellement être utilisé par d'autres projets. Il est donc important de s'assurer de son indépendance vis-à-vis du reste du projet et de la cohérence des missions qu'il assure. De plus, il est important de le documenter convenablement afin de le rendre compréhensible et facilement réutilisable.

Avec Python

Il existe un grand nombre de modules natifs au moteur Python. La liste exhaustive est disponible dans la Doc Python en ligne. Parmi eux, en voici quelques-uns particulièrement utilisés :

Il en existe beaucoup d'autres, développés par la grande communauté des développeurs Python et qui nécessiteront une installation supplémentaire. Ils sont en général accompagnés d'un site internet les documentant :

En python, les modules sont importés grâce au mot clé import proposant plusieurs syntaxes :

Syntaxe 1 :

from math import *
print(pi)
	
Syntaxe 2 :

from math import pi
print(pi)
	
Syntaxe 3 :

import math
print(math.pi)
	
Syntaxe 4 :

import math as m
print(m.pi)
	

Attention, la première syntaxe importe directement dans l'espace des noms du programme, tout ce que contient le module (*). Cela peut entraîner une surcharge de nom, c'est-à-dire l'écrasement d'une variable ou d'une fonction par une autre varible ou une autre fonction portant le même nom.
Ce risque de surcharge est facilement évitable à l'aide des autres syntaxes. En n'important qu'une fonction du module (syntaxe 2) ou en nommant explicitement le module (syntaxe 3) ou en le renommant (syntaxe 4) lors de l'utilisation d'une de ses fonctions.

Pour créer vos propres modules, il suffit de créer un fichier python mon_module.py à la racine de votre projet et de l'importer avec les mêmes syntaxes proposées par l'instruction import.


from mon_module import *
from mon_module import ma_fonction
import mon_module
import mon_module as mm

Pour un site internet

Dans le cas d'un site internet, la modularité est déjà prise en compte dans le découpage fonctionnel des fichiers HTML, CSS et JS. Pour rappel, les fichiers HTML hébergeant le contenu du site, les fichiers CSS, sa mise en page et les fichiers JS assurant son interactivité.

Comme pour la plupart des langages de programmation, la modularité est également permise en javascript. Par exemple sur ce site de cours de NSI, la mise en page des codes est sous-traitée à un module (fichier javascript) intitulé prism.js utilisant également un fichier CSS prism.css. Ces deux fichiers ont été téléchargés depuis le site prismjs.com et intégrés aux fichiers du site.

De la même façon qu'en python, les modules sont importés en début de script, nous trouverons dans la balise <head> d'une page HTML, des liens vers des fichiers javascript. Ces fichiers peuvent tout aussi bien être locaux (repéré par leur chemin relatif) ou bien hébergé ailleurs sur internet (repéré par leur URL).


<head>
	<meta charset="utf-8" />
	<title>C9-Programmation</title>
	<link rel="stylesheet" type="text/css" href="styles/style.css" />
	<link rel="stylesheet" type="text/css" media="print" href="styles/print.css"/>
	<link href="styles/prism.css" rel="stylesheet" />

	<script src="scripts/interactif.js" defer></script>
	<script src="scripts/prism.js" defer></script>
	<script async="true" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=AM_CHTML" defer></script>
</head>
Repérer, dans le code HTML précédent, un module hébergé sur un autre serveur. À quoi sert-il ? Il s'agit du module MathJax.js hébergé sur https://cdn.jsdelivr.net. Son rôle est d'assurer l'affichage correct d'équations mathématiques écrites en AsciiMath dans le code source de la page. Le texte AsciiMath doit être encadré par des accents graves.
Par exemple : le code HTML suivant x = y^2 + sqrt(y)/3 s'affichera : `x = y^2 + sqrt(y)/3`. La syntaxe de l'AsciiMath est détaillée sur http://asciimath.org/.

Gestion des bugs

Le terme "bug" a été inventé par Grace Hopper en référence à un insecte coincé dans un ordinateur en 1974 et qui avait généré un problème.

En programmation, savoir répondre aux causes de bugs et tester ses scripts est essentiel pour obtenir un programme fiable et robuste.

Environnement de Développement Intégré

Un bon Environnement de Développement Intégré (EDI) fournit plusieurs outils permettant de limiter l'apparition de bugs. Il doit permettre :

Conçu pour le langage python, la version PyCharm Community est un EDI libre, gratuit et assez ergonomique qui propose ces fonctionnalités.
Vous pouvez les retrouver également dans Visual Studio Code qui est un EDI généraliste (plusieurs langages) et qui propose de nombreuses extensions très intéressantes.

Liste non-exhaustive de conseils pour éviter les bugs

  1. Corriger tout de suite les bugs évidents, mis en relief par votre EDI.
  2. Prévoir toutes les saisies possibles d'un utilisateur et imaginer le pire pour rendre votre code robuste.
  3. Créer et appliquer des jeux de tests (avec des assertions par exemple) vérifiant tous les cas d'utilisation de votre code :
    • les cas limites (le zéro, une chaîne de caractère vide ou une liste vide, le premier ou le dernier élément d'une séquence ...).
    • tous les cas d'une structure conditionnelle.
  4. En python, il faudra faire bien attention à l'indentation. Un programme mal indenté peut donner une réponse éronnée sans lever d'erreur.
  5. Éviter les effets de bord qui sont sources de nombreuses erreurs. Pour cela il faut éviter d'utiliser des variables globales et des fonctions qui modifient des variables externes. Utiliser une dose de programmation fonctionnelle.
  6. Éviter les comparaisons entre flottants.
  7. Donner des noms intelligibles à vos variables, vos fonctions, vos classes ...
  8. Dans le cas des boucles non-bornées while:
    • s'assurer que l'on puisse entrer dans la boucle (condition vraie).
    • s'assurer que la condition puisse devenir fausse, on parle alors de condition d'arrêt, afin d'éviter les boucles infinies. Pour cela, le variant de boucle doit évoluer à chaque itération de la boucle vers la condition d'arrêt.
  9. Dans le cas des boucles bornées for, il faudra éviter pendant l'exécution de la boucle de modifier la liste que l'on est en train de parcourir.
  10. Tester vos scripts (fonctions, modules) en cours de développement avec des assertions.
  11. Appliquer une batterie de test avant de livrer votre code.
Repérer le bug que génère le programme suivant et y apporter une correction pour l'éviter.

a = 0.0
while a != 0.3:
    a += 0.1
    print(a)
print("C'est fini !")
	
Dans ce cas, nous tombons dans une boucle infinie car a n'est jamais égal à 0.3. Le problème vient de l'approximation des flottants et ici de la comparaison dans la boucle while.
Ici la correction consiste à s'assurer que le variant de boucle (ici a) bascule définitivement la condition comme fausse à l'aide d'un strictement inférieur.

a = 0.0
while a < 0.3:
    a += 0.1
    print(a)
print("C'est fini !")
Une autre solution si l'on a vraiment besoin de vérifier l'égalité de flottants est d'utiliser un encadrement de la valeur dans la condition.

a = 0.0
while abs(0.3 - a) > 1e-3:
    a += 0.1
    print(a)
print("C'est fini !")
Repérer l'erreur que génère le programme suivant et y apporter une correction pour l'éviter.

def supprimer_les_nombres_pairs(liste):
    for i in range(len(liste)):
        if liste[i]%2 == 0:
            liste.pop(i)
    return liste

liste = [i for i in range(10)]
print(supprimer_les_nombres_pairs(liste))
	
Dans ce cas, l'erreur est IndexError: list index out of range. En effet, la liste est modifiée au cours des itérations de la boucle or vient un moment où l'indice demandé ne renvoie plus rien car la liste est plus courte qu'au départ de la boucle.
Une possibilité est de créer une seconde liste que l'on rempli avec seulement les éléments retenus.

def supprimer_les_nombres_pairs(liste):
    nouvelle_liste = []
    for el in liste:
        if el%2 != 0:
            nouvelle_liste.append(el)
    return nouvelle_liste
	
Attention mauvaise solution : Ici la fonction enumerate(liste) ajoute un compteur à la liste permettant d'accéder à des tuples (indice, valeur). Ce compteur s'adapte à la taille de la liste pour empêcher l'erreur IndexError.

def supprimer_les_nombres_pairs(liste):  # Mauvaise correction !
    for i,el in enumerate(liste):
        if el%2 == 0:
            liste.pop(i)
    return liste
	
Bien que dans certains cas liste = [0, 1, 2, 3], la réponse soit correcte, cette solution est totalement fausse. En effet, la liste est toujours en train d'être modifiée au cours des itérations et si elle répond bien ce n'est pas quelle a gardé les nombres impairs mais qu'elle les a sauté dans son parcours de la liste du fait que celle-ci est racourcie au fur et à mesure. Vérifier que ce script donne une mauvaise réponse pour liste = [0, 2, 1, 3].

Celle-ci reprend l'idée de créer une nouvelle liste et de la remplir avec les éléments retenus mais en utilisant une construction par compréhension.

def supprimer_les_nombres_pairs(liste):
    return [element for element in liste if element%2!= 0]
	
Dans cette solution, il faut appliquer la fonction list() à l'objet renvoyé par la fonction filter() afin de le convertir en liste.

def supprimer_les_nombres_pairs(liste):
    return list(filter(lambda x: x%2 != 0, liste))
	

Les exceptions en python

Quand le moteur Python rencontre une erreur, il lève une exception. Une exception est un objet en python qui est définie par son type.
Voici une liste non exhaustive de types d'exception :

Type d'erreur Description
SyntaxError Le code ne peut pas être exécuté car l'analyseur a repéré une erreur dans la syntaxe de python. Il peut s'agir
de deux-point ":" manquant, de parenthèses mal couplées ou d'indentation manquante. L'analyseur qui ne reconnaît
plus la syntaxe de python, signale une ligne d'erreur mais attention la faute de syntaxe est souvent localisée
avant cette ligne.
ZeroDivisionError Erreur de division par zéro.
NameError Une variable, une fonction, une classe, invoquée ... n'est pas précédement définie.
TypeError L'opérateur n'accepte pas le type d'opérande qui lui est donné.
ValueError Ici l'opérateur reçoit le bon type d'opérande mais sa valeur est inappropriée.
IOError Le fichier n'existe pas.
RecursionError La limite de la pile d'appels récursifs a été atteinte. Elle est de 1000 par défaut et peut être modifiée.
IndexError L'indice pour une séquence, ou la clé pour un dictionnaire n'existe pas.
AssertionError Une assertion est fausse.
KeyboardInterrupt L'exécution est stoppée par l'utilisateur (fermeture de fenêtre par exemple). Contrairement aux autres erreurs, il ne s'agit pas d'un objet Exception, en python. Par exemple, il ne sera pas capter par l'instruction except Exception :, contrairement à toutes les autres. Par contre, il sera capter par l'instruction except :.
Quel type d'erreur risque de lever ce script ?

x = int(input("Entrez un nombre : "))
		
ValueError car la fonction int() est incapable de convertir une chaîne de caractère qui contiendrait autre chose que des entiers.
Quel type d'erreur va lever ce script ?

age = input("Entrez votre age : ")
if age >= 18:
    print("Vous êtes majeur.")
		
TypeError car l'opérateur >= ne sait pas comparer une chaîne de caractères avec un entier.

Gestion des exceptions en python

Python propose une syntaxe afin de gérer les erreurs.


try:
    # instructions à exécuter
except """type d'erreur 1""" :
    # instructions à exécuter si ce type d'erreur est levée
except """type d'erreur 2""" :  # Un deuxième except est facultatif
    # instructions à exécuter si ce type d'erreur est levée
else:  # Factultatif
    # instructions à exécuter s'il n'y a pas eu d'erreur
finally:  # Facultatif
    # instructions à exécuter dans tous les cas même si un return existe dans un bloc except

Remarque : Il est possible de ne pas préciser le type d'erreur à attraper après except:, mais cela est déconseillé car toutes les erreurs levées ne se valent pas et cela peut être source d'erreurs supplémentaires en capturant une erreur qui n'aurait pas dû l'être et qui sera alors invisible.

Compléter/modifier le script suivant avec des instructions try: except: else: afin qu'aucune erreur ne soit levée quelque soit l'action de l'utilisateur et qu'il soit informé de son éventuelle erreur.

age = int(input("Entrez votre age : "))
if age >= 18:
    print("Vous êtes majeur.")
else:
    print("Vous êtes mineur.")		
		

try :
    age = input("Entrez votre age : ")
except KeyboardInterrupt:
    pass  # Instruction vide : rien ne s'exécute
else:	
    try:
        age = int(age)
    except ValueError:
        print("Vous devez entrer que des caractères numériques.")
    else:
        if age >= 18:
            print("Vous êtes majeur.")
        else:
            print("Vous êtes mineur.")
	

Il est également possible de lever soi-même une erreur à l'aide du mot clé raise ou bien encore de récupérer une erreur dans une variable.


Tester l'instruction : raise NameError("Ce nom de variable n'existe pas."). La console affiche : NameError: Ce nom de variable n'existe pas.
Compléter/modifier le script suivant en levant les bonnes exceptions et en les capturant pour afficher un message clair à l'utilisateur.

def majeur(age):
    if type(age) != type(1):
        raise # à compléter
    if age < 0:
        raise # à compléter
    if age >= 18:
        return "majeur"
    return "mineur"

age = -1
try:
    print("Vous êtes {0}.".format(majeur(age)))
except TypeError as er:
    print("Erreur :", er)
except ValueError as er:
    print("Erreur :", er)
		

def majeur(age):
    if type(age) != type(1):
        raise TypeError("L'age doit être de type entier.")
    if age < 0:
        raise ValueError("L'âge doit être un entier positif.")
    if age >= 18:
        return "majeur"
    return "mineur"

age = -1
try:
    print("Vous êtes {0}.".format(majeur(age)))
except TypeError as er:
    print("Erreur :", er)
except ValueError as er:
    print("Erreur :", er)
	

L'utilisation des assertions dans une structure de gestion des erreurs permet de personnaliser encore davantage les messages d'erreur.


age = input("Entrez votre âge :")
try:
    age = int(age)
    assert age >= 0, "Vous ne pouvez pas avoir un âge négatif."
    assert age < 200, "Vous ne pouvez pas avoir plus de 199 ans."
except ValueError:
    print("Vous devez entrer uniquement des caractères numériques.")
except AssertionError as msg :
    print(msg)

Le typage des variables

En apparence, python ne se soucie pas des types de variable : lorsqu'on initialise une variable, à aucun moment on ne doit déclarer son type et à tout moment, on peut écraser le contenu d'une variable par une valeur d'un autre type. C'est assez pratique car cela allège le code mais au risque de générer des erreurs.


>>> a = "Hello, World"
>>> print(type(a))
<class str>
>>> a = 3 + 2
>>> print(type(a))
<class int>	

Cet exemple montre que Python connaît bien le type de ses variables et qu'il vous autorise à en changer comme bon vous semble. Le risque est d'affecter de mauvaises valeurs à une variable sans s'en rendre compte et ainsi génerer des bugs. Certains langages comme le C oblige à déclarer au préalable les variables et leurs types. Le compilateur va alors détecter les erreurs de typage et avertir le développeur des problèmes avant même qu'ils ne se produisent mais ce n'est pas le cas en python.

Il est possible de réaliser à partir de python 3.10 des annotations de type, qui sont utiles pour le développeur. L'Environnement de Développement Intégré pourra les exploiter pour avertir en cas d'erreurs ou faire des suggestions pertinentes limitant le risque d'erreurs.

Capture d
Capture d'écran du PyCharm

Exemple : Les deux fonctions écrites ci-dessus dans PyCharm sont fonctionnellement équivalentes mais la seconde comporte des informations de type utilisé par l'EDI pour aider le développeur, comme illustré ci-dessous.

Gestion du code avec git et GitHub

Le contrôle de versions permet de suivre les modifications, faites par qui et pourquoi, ce qui devient crucial pour de gros projets et le travail d'équipe.

Voir le cours d'OpenClassrooms

The Zen of Python (PEP 20)

Après ces deux années sur python, vous êtes devenu un Maître Python et vous êtes dignes de transmettre à votre tour le Zen du Python !
Exécuter la ligne de commande import this dans la console python pour y accéder.
  1. Préfèrer le beau au laid,
  2. l'explicite à l'implicite
  3. le simple au complexe,
  4. le complexe au compliqué,
  5. le déroulé à l'imbriqué,
  6. l'aéré au compact.
  7. La lisibilité compte.
  8. Les cas particuliers ne le sont jamais assez pour violer les règles,
  9. … même s'il faut privilégier la praticité à la pureté.
  10. Ne jamais passer les erreurs sous silence,
  11. … ou les faire taire explicitement.
  12. En cas d'ambiguïté, résister à la tentation de deviner.
  13. Il devrait y avoir une, et de préférence une seule, façon évidente de procéder,
  14. … même si cette façon n'est pas évidente à première vue, à moins d'être Hollandais.
  15. Mieux vaut maintenant que jamais,
  16. … même si jamais est souvent préférable à immédiatement.
  17. Si l'implémentation s'explique difficilement, c'est une mauvaise idée.
  18. Si l'implémentation s'explique facilement, c'est peut-être une bonne idée.
  19. Les espaces de noms sont une sacrée bonne idée, utilisons-les plus souvent !



Bibliographie :