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

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...).
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 , 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, matériellement la mémoire, 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
vonneumann.jpg
, 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 constituée d' :

  • un ruban (comme le ruban magnétique d'une cassette), de longueur infinie des deux côtés, découpé en cases identiques que l'on peut lire ou sur lesquelles on peut écrire des caractères (L'alphabet peut être librement choisi. Des cases peuvent aussi rester vides et sont en général repérées par le caractère #).
  • une tête de lecture qui pointe sur une case donnée ; celle-ci permet de lire et d'écrire sur la case, et peut aussi, éventuellement, se déplacer d'une case vers la gauche (symbole : ←) ou vers la droite (symbole : →).
  • un «cœur» de la machine (graphe appelé automate déterministe) :
    • Un ensemble fini d'états, entre lesquels on va évoluer ; ces états peuvent être vus comme les sommets d'un graphe. On sélectionne parmi eux :
      • un état initial (la machine part de cet état, en orange sur les figures). Il est souvent repéré par une flèche entrante ;
      • un ou plusieurs état(s) final(aux) (la machine effectue sa ou ses dernière(s) opération(s), puis s'arrête, lorsqu'elle arrive sur un tel état (en violet sur les figures). On parle aussi d'états acceptants, ou terminaux ; ils sont souvent repérés par une flèche sortante.
    • une table d'actions (finie aussi), qui indique, en fonction de l'état courant de la machine et du caractère inscrit sur le ruban en face de la tête de lecture, quel est le nouvel état, quel nouveau symbole éventuellement inscrire sur le ruban, et quel déplacement effectuer de la tête de lecture (←, ne pas bouger ou bien →).
      Cette table d'action peut être représentée par les arêtes orientées d'un graphe, chacune étiquetée par un ou plusieurs triplets : (si caractère lu, écrire caractère, déplacement éventuel). On parle de table d'action, car on peut écrire la liste d'adjacence du graphe obtenu sous forme de tableau (c'est moins lisible, cependant).
La machine de Turing s'arrête dans deux cas :
  • elle est dans un état où elle ne peut rien faire pour continuer. Dans ce cas, on dit que le mot de départ sur le ruban est refusé.
  • elle est entrée dans un état étiqueté comme final. Dans ce cas, on dit que le mot de départ sur le ruban est accepté.
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 ci-contre, 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).
  1. 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.
  2. Qu'est devenu ce mot ? (état final du ruban)
    Le mot initial est devenu #bbab#.
  3. Le mot aaabbb est-il accepté ?
    Ce mot est accepté : on s'arrête sur l'état final q4.
  4. Qu'est devenu ce mot ?
    L'état final du ruban est ########.
machine-turing-1.png
La machine de Turing permet de «» des calculs. On va montrer que la fonction «» (pour les nombres entiers positifs) est calculable, ainsi que l'addition.
Pour cela, nous avons besoin de la représentation 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 #x̄# = #1…1# avec q1 pour état initial, et avec x un entier naturel quelconque, écrit en unaire sur le ruban (la tête de lecture, comme précisé par la notation, pointe sur le premier # à gauche).

machine-turing-2.png
  1. Si x = 0, que se passe-t-il ? Le mot de départ est-il accepté ou refusé ?
    On suit toutes les flèches vers la droite pour arriver à l'état final : 0 est accepté.
  2. Que fait cette machine pour x ≥ 1 ?
    On obtient le mot #1###...#, qui correspond à 0.
  3. 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.
  4. Écrire la table d'action de cette machine (sous forme de tableau).
    état courant symbole lu symbole écrit dé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 #x̄#ȳ#, où x et y sont des entiers naturels, écrits en unaire sur le ruban avec q1 comme état initial, et la tête de lecture pointe sur le premier # à gauche.
On pose z = x + y. Construire une machine de Turing permettant d'écrire #z̄# ; on prouve ainsi que l'addition des entiers est une opération calculable.
machine-turing-3.png
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. 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

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
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é.
diag-compilateur.png
Il s'oppose à l'interpréteur, qui est un logiciel qui exécute directement le code source d'un langage (interprété).
diag-interpreteur.png
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.
diag-java.png
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.
niveau.png

Paradigmes de programmation

Le terme paradigme signifie à la fois :
  • vision du monde : modéliser une situation pour la transformer en programme ;
  • méthode de travail : la présentation formelle qui va traduire cette vision.

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 , 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.

  1. 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.
  2. 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
  3. 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 :
  1. 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
  2. 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
  3. 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()))
  4. 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.

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ées 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 :

  • Le découpage en sous-parties indépendantes permet la réutilisation du code pour d'autres projets.
  • La structuration d'un projet en plusieurs sous-projets, rend le projet inital plus réalisable, compréhensible et maintenable.
  • La modularité est une nécessité pour l'organisation d'un projet et le partage des tâches au sein d'une équipe.

Cette modularité existe à plusieurs échelles :

  • Dans un programme d'une centaine de lignes regroupées dans un seul fichier, elle apparaît sous la forme d'importation de modules/bibliothèques en début de script ou bien dans la conception de fonctions réutilisables.
  • Pour un programme d'un millier de lignes, l'intérêt de le découper en plusieurs fichiers apparaît pour en facilité l'écriture et la maintenance, pour isoler une classe d'objet particulière ou un ensemble de fonctions placées dans un fichier auxiliaire ou pour séparer une interface graphique du coeur du programme dans deux fichiers distincts.
  • Pour un projet plus conséquent de 10 000 lignes, la modularité permet d'organiser le code, de permettre le développement en parallèle de plusieurs parties.

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 :

Module description
tkinter interface graphique
math fonction et constantes mathématiques
random génération de nombres aléatoires
statistics fonctions de mathématiques statistiques
time fonctions liées au temps
os interaction avec le système d'exploitation, gestion des fichiers
smtplib interaction avec un serveur SMTP
turtle affichage graphique
sqlite3 interaction avec une BD
pip gestion et installation de modules supplémentaires
PIL traitement d'image
NumPy calcul numérique et matriciel
Matplotlib tracé graphique

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 cdn.jsdelivr.net. Son rôle est d'assurer l'affichage correct d'équations mathématiques écrites en LaTeχ ou bien AsciiMath dans le code source de la page. Le texte LaTeχ/AsciiMath doit être encadré par $...$ ou bien \(...\).
Par exemple : le code HTML suivant $x = y^2 + \frac{\sqrt{y}}{3}$ s'affichera : x=y2+y3.
La syntaxe de l'AsciiMath est détaillée sur asciimath.org et celle de LaTeχ sur latex-project.org.

Gestion des bugs

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

3.3.1. 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 :

  • la complétion intelligente du code pour éviter les erreurs de frappe,
  • l'inspection du code avec la mise en évidence d'erreurs et leur correction rapide,
  • la refactorisation du code et une navigation riche pour en faciliter la relecture,
  • l'exécution en mode debug, c'est-à-dire pas à pas avec affichage de l'état courant des variables, afin de diagnostiquer un bug.

Conçu pour le langage python, la version PyCharm Community est un libre, gratuit et assez ergonomique qui propose ces fonctionnalités.
Vous pouvez les retrouver également dans Visual Studio Code qui est un 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 .
  2. Prévoir toutes les saisies possibles d'un utilisateur et imaginer le pire pour rendre votre code .
  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 remplit 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 raccourcie 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.

  1. Tester l'instruction : raise NameError("Ce nom de variable n'existe pas.").
    La console affiche : NameError: Ce nom de variable n'existe pas.
  2. 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 obligent à 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.

Exemple : Ces deux fonctions sont fonctionnellement équivalentes mais la seconde comporte des informations de type utilisé par l'EDI pour aider le développeur.

def triple(x):
    return 3 * x

def triple_v2(x: int) -> int:
    return 3 * x

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 la Documentation de Git

3.5. Zen

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 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 !

3.6. Bibliographie