Programming errors

Reading time10 min

En bref

Résumé de l’article

Dans cet article, nous analysons certaines erreurs qui peuvent survenir lors de l’écriture d’un programme, et discutons du cas particulier des programmes avec des éléments aléatoires.

Points clés

  • Il existe deux types d’erreurs : les erreurs syntaxiques et les erreurs d’exécution.

  • Les nombres aléatoires peuvent rendre le débogage difficile, il est donc recommandé de fixer une graine (seed).

Contenu de l’article

La programmation est un processus incrémental, ce qui signifie que le code doit être construit étape par étape, chaque étape devant être testée avant de passer à la suivante. Le temps de développement peut être grossièrement divisé en trois phases itératives :

  • Codage.
  • Débogage.
  • Documentation.

Et le débogage prend définitivement beaucoup de temps de développement. Une compétence importante à acquérir est d’identifier efficacement la source des bugs et de les corriger. De nombreux outils ont été conçus pour vous aider à déterminer rapidement où se trouvent les erreurs de programmation et les bugs. Mais avant de découvrir ces outils spécifiques, il est obligatoire d’apprendre à interpréter les messages d’erreur rendus par l’interpréteur (resp. compilateur).

L’interpréteur Python peut détecter des erreurs lors de l’interprétation de votre code qui sont principalement de deux types.

1 — Erreurs syntaxiques

Les erreurs syntaxiques sont situées dans le texte de votre code, et peuvent être détectées sans exécuter le code (pendant les phases d’analyse/compilation). Elles sont assez explicitement expliquées par l’interpréteur qui vous fournit une description de l’erreur et la ligne où elle se produit.

Exemple

Considérez le programme suivant :

# The string to manipulate
s = "IMT Atlantique"
print(s)

# Loop to append to a new string
result = ""
for i j in range(len(s)):
    result = s[i] + result

# Print the result
print("Reversed string:", result)
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This is the entry point of your program.
     * It contains the first codes that are going to be executed.
     * 
     * @param args Command line arguments received.
     */
    public static void main(String[] args) {
        // The string to manipulate
        String s = "IMT Atlantique";
        System.out.println(s);
        
        // Loop to append to a new string
        String result = "";
        for (int i j = 0; i < s.length(); i++) {
            result = s.charAt(i) + result;
        }

        // Print the result
        System.out.println("Reversed string: " + result);
    }

}

Ici, l’interpréteur nous indique qu’à la ligne 7 de votre code, le token j est inattendu à cette position, car il ne peut pas suivre un autre token correspondant à un nom de variable (i.e., i) :

Sortie
File "session_1.py", line 7
  for i j in range(len(s)):
          ^
SyntaxError: invalid syntax
Main.java:25: error: ';' expected
        for (int i j = 0; i < s.length(); i++)
                  ^
Main.java:25: error: not a statement
        for (int i j = 0; i < s.length(); i())
                            ^
Main.java:25: error: ')' expected
        for (int i j = 0; i < s.length(); i())
                                        ^
Main.java:25: error: ';' expected
        for (int i j = 0; i < s.length(); i())
                                             ^
4 errors

2 — Erreurs d’exécution

Les erreurs d’exécution apparaissent uniquement lorsque le programme est en cours d’exécution (pendant la phase d’exécution). Elles sont plus difficiles à résoudre car elles dépendent souvent du contexte d’exécution. Voici quelques exemples pouvant causer ce type d’erreur :

  • Une opération non autorisée compte tenu des types actuels des valeurs concernées.
  • Une opération d’entrée/sortie sur une ressource indisponible (ex., un fichier).
  • Un accès à une zone mémoire indisponible.
Exemple

Considérez le programme suivant :

# Ask user at runtime for a value
den = input("Please enter the value of the denominator")

# Perform a computation
result = 42 / den
print(result)
// Needed imports
import java.util.Scanner;

/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This is the entry point of your program.
     * It contains the first codes that are going to be executed.
     * 
     * @param args Command line arguments received.
     */
    public static void main (String[] args) {
        // Ask user at runtime for a value
        Scanner scanner = new Scanner(System.in);
        System.out.print("Please enter the value of the denominator: ");
        String denStr = scanner.nextLine();
        double den = Double.parseDouble(denStr);

        // Perform a computation
        double result = 42 / den;
        System.out.println(result);
    }

}

Bien que syntaxiquement correct, la troisième ligne du code suivant générera une erreur d’exécution de type division par zéro si 0 est assigné à den.

3 — Le cas des nombres aléatoires

Parfois, les programmes utilisent des nombres aléatoires. Considérez par exemple le programme suivant :

# Needed imports
import random

# Fill a list with 10 random numbers
numbers = [random.randint(-10, 10) for i in range(10)]

# Generate an error if 0 is in the list
if 0 in numbers:
    raise ValueError("Zero is in the list")

# Otherwise, print the list
print(numbers)
// Needed imports
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This is the entry point of your program.
     * It contains the first codes that are going to be executed.
     * 
     * @param args Command line arguments received.
     */
    public static void main (String[] args) {
        // Create a random number generator
        Random random = new Random();

        // Fill a list with 10 random numbers
        List<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            numbers.add(random.nextInt(11) - 10);
        }

        // Generate an error if 0 is in the list
        if (numbers.contains(0)) {
            throw new IllegalArgumentException("Zero is in the list");
        }

        // Otherwise, print the list
        System.out.println(numbers);
    }

}

Parfois, lors de l’exécution de votre programme, vous obtiendrez la sortie suivante :

Sortie
[3, 1, -2, -4, 8, 4, -9, -2, 3, 3]
[-9, -1, -6, -7, -4, -7, -10, -8, -9, -10]

Ou parfois :

Sortie
[-2, -7, -9, -1, -6, -7, -7, -6, -2, -7]
[-5, -4, -3, -9, -10, -5, -1, -1, -7, -5]

Ou :

Sortie
Traceback (most recent call last):
  File "session_1.py", line 9, in <module>
    raise ValueError("Zero is in the list")
ValueError: Zero is in the list
Exception in thread "main" java.lang.IllegalArgumentException: Zero is in the list
	at Main.main(Main.java:37)

Eh bien, c’est attendu. Cependant, imaginez un scénario plus complexe où l’erreur factice ci-dessus est une erreur vraiment importante que vous devez éviter. Il sera assez difficile à déboguer, car parfois votre programme fonctionnera comme prévu, et parfois il plantera à cause d’une erreur sémantique.

En pratique, il est possible de forcer une série de nombres aléatoires à toujours être la même. En effet, les nombres aléatoires sont en fait pseudo-aléatoires. Ils sont générés en utilisant une série déterministe qui possède de bonnes propriétés d’aléa, initialisée à l’aide d’une “graine” (seed), i.e., une première valeur initiale. Si la graine est connue, alors toute la série peut être prédite. Mieux encore, si la graine peut être fixée à une valeur arbitraire, alors on peut garantir que toutes les exécutions du même programme conduiront à la même génération de nombres aléatoires.

Chaque générateur de nombres aléatoires fournit un moyen de fixer la graine à une valeur donnée (ici, 42). Cela peut être fait comme suit :

# Needed imports
import random

# Set the seed
random.seed(42)

# Fill a list with 10 random numbers
numbers = [random.randint(-10, 10) for i in range(10)]

# Generate an error if 0 is in the list
if 0 in numbers:
    raise ValueError("Zero is in the list")

# Otherwise, print the list
print(numbers)
// Needed imports
import java.util.Random;

/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This is the entry point of your program.
     * It contains the first codes that are going to be executed.
     * 
     * @param args Command line arguments received.
     */
    public static void main (String[] args) {
        // Create a random number generator
        Random random = new Random();

        // Set the seed
        random.setSeed(42);

        // Fill a list with 10 random numbers
        List<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            numbers.add(random.nextInt(11) - 10); 
        }

        // Generate an error if 0 is in the list
        if (numbers.contains(0)) {
            throw new IllegalArgumentException("Zero is in the list");
        }

        // Otherwise, print the list
        System.out.println(numbers);
    }

}

Relançons notre programme trois fois encore

Sortie
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
Exception in thread "main" java.lang.IllegalArgumentException: Zero is in the list
	at Main.main(Main.java:36)
Sortie
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
Exception in thread "main" java.lang.IllegalArgumentException: Zero is in the list
	at Main.main(Main.java:36)
Sortie
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
Exception in thread "main" java.lang.IllegalArgumentException: Zero is in the list
	at Main.main(Main.java:36)

Comme vous le voyez, le résultat est toujours le même. Maintenant, ce que vous pouvez faire pour déboguer votre programme est de trouver une valeur de la graine qui conduit à une erreur. Ensuite, gardez cette graine fixée et essayez de voir ce qui se passe avec cette configuration aléatoire problématique particulière !

Important

Quelques remarques importantes :

  • La liste que vous obtenez peut être différente d’un ordinateur à un autre, car le générateur de nombres aléatoires utilisé peut être différent. Fixer la graine ne garantit la reproductibilité que sur une seule machine.

  • Chaque générateur de nombres aléatoires possède sa propre graine. Si vous utilisez des fonctions des bibliothèques random et numpy.random par exemple, vous devrez fixer les graines des deux générateurs pour garantir la reproductibilité.

  • Vous ne pouvez pas utiliser la graine aléatoire pour modifier les performances de vos programmes. Par exemple, si votre algorithme qui incorpore des éléments aléatoires résout un problème mieux avec seed = 42 qu’avec seed = 0, il est injuste de rapporter les performances obtenues dans le premier cas. En effet, cela signifie simplement que 42 conduit à une initialisation chanceuse de votre algorithme pour cette instance particulière du problème, mais ce ne sera pas le cas en général. Le rapport des performances pour les algorithmes aléatoires doit toujours être fait en exécutant le programme de nombreuses fois et en fournissant des moyennes et des intervalles de confiance !

Pour aller plus loin

On dirait que cette section est vide !

Y a-t-il quelque chose que vous auriez aimé voir ici ? Faites-le nous savoir sur le serveur Discord ! Peut-être que nous pourrons l’ajouter rapidement. Sinon, cela nous aidera à améliorer le cours pour l’année prochaine !

Pour aller au-delà

  • Logique de Hoare.

    C’est une manière formelle de décrire le comportement d’un programme pour montrer sa correction.

  • Design by contract.

    Un paradigme de programmation reposant sur les notions ci-dessus pour concevoir des logiciels.