Practical session

Duration3h45

Présentation & objectifs

Lors de cette séance, vous résoudrez des exercices pour vous familiariser avec les exceptions défensives et les assertions, ainsi que leur utilisation pour la programmation défensive. Vous écrirez également vos premiers tests unitaires, et vous concentrerez sur la documentation.

Important

Le but de cette séance est de vous aider à maîtriser des notions importantes en informatique. Un assistant de programmation intelligent tel que GitHub Copilot, que vous avez peut-être déjà installé, pourra vous fournir une solution à ces exercices uniquement à partir d’un nom de fichier judicieusement choisi.

Pour des raisons de formation, nous vous conseillons de désactiver d’abord ces outils.

À la fin de l’activité pratique, nous vous suggérons de retravailler l’exercice avec ces outils activés. Suivre ces deux étapes améliorera vos compétences à la fois fondamentalement et pratiquement.

De plus, nous vous fournissons les solutions des exercices. Assurez-vous de ne les consulter qu’après avoir une solution aux exercices, à des fins de comparaison ! Même si vous êtes sûr que votre solution est correcte, veuillez les consulter, car elles fournissent parfois des éléments supplémentaires que vous auriez pu manquer.

Contenu de l’activité

1 — Extraire une liste de flottants

Nous avons une liste l contenant des éléments de différents types. Par exemple : l = [1, '3.14', 2.8, [], (2, 5), 'hello', {'d': 3, 'e': 2.145}].

Nous avons également la fonction suivante qui extrait tous les flottants d’une liste donnée pour alimenter et retourner une nouvelle liste floats_in_list :

# Needed imports
from typing import List, Any



def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:
        if type(value) in (int, float):
            floats_in_list.append(float(value))
    return floats_in_list

Testez cette fonction sur la liste d’exemple l ci-dessus. Que remarquez-vous ?

Réécrivez la fonction pour prendre en compte les nombres sous forme de chaînes de caractères. Pour cela, vous pouvez utiliser la propriété string.digits.

Solution à l’exercice
# Needed imports
from typing import List, Any
import string



def is_integer (s: str) -> bool:

    """
        This function verifies if a given string represents an integer.
        In:
            * s: The string to check.
        Out:
            * True if it is an integer in a string, False otherwise.
    """

    # Check that everything in the string is a digit
    for char in s:
        if char not in string.digits:
            return False
    return True



def is_number (s: str) -> bool:

    """
        This function verifies if a given string represents an integer or a float.
        In:
            * s: The string to check.
        Out:
            * True if it is a number in a string, False otherwise.
    """

    # If it is an integer we are done
    if is_integer(s):
        return True
    
    # Otherwise, let's check if it is a float
    i = s.find('.')
    if i != -1:
        return is_integer(s[:i]) and is_integer(s[i+1:])

    # No point found, it is something else
    return False

        

def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:
        if type(value) in (int, float) or (type(value) is str and is_number(value)):
            floats_in_list.append(float(value))
    return floats_in_list



# Test the function
if __name__ == "__main__":
    l = [1, '3.14', 2.8, [], (2,5), 'hello', {'d': 3, 'e': 2.145}]
    print(list_of_floats(l))

Utilisez le mécanisme des exceptions pour obtenir le même résultat. Testez sur les listes l et l2 = [(2, 5), 'hello', {'d': 3, 'e': 2.145}].

Solution à l’exercice
# Needed imports
from typing import List, Any



def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:

        # Try some conversion and capture errors to determine type
        try:
            floats_in_list.append(float(value))
        except ValueError as e:
            print(f"ValueError: Could not convert {value} to float. {e}")
        except TypeError as e:
            print(f"TypeError: Invalid type for {value}. {e}")

    return floats_in_list



# Test the function
if __name__ == "__main__":
    l1 = [1, '3.14', 2.8, [], (2, 5), 'hello', {'d': 3, 'e': 2.145}]
    print(listOfFloats(l1))
    l2 = [(2, 5), 'hello', {'d': 3, 'e': 2.145}]
    print(listOfFloats(l2))

2 — Argmax

La fonction argmax retourne l’indice de la (première occurrence de la) valeur maximale apparaissant dans une liste de valeurs, si la liste est vide ou nulle (None ou null), elle retourne -1.

Écrivons des tests unitaires pour cette fonction. Voici un exemple de test pour la fonction (copiez-collez-le dans un fichier tests_argmax.py), en supposant que argmax se trouve dans un fichier argmax.py :

# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,4,2,0}));
    }
}

Vous allez coder la fonction argmax(lst) et écrire d’autres tests pour vous assurer qu’elle fonctionne correctement. Avant de coder, vous devez lister quelques exemples uniques que vous pourrez également utiliser pour vos tests. Vous devez aussi documenter la fonction et, si nécessaire, commenter le code.

Important

L’instruction unittest.main() exécute les tests définis dans le fichier. La bonne pratique est de séparer les fonctions et les tests. N’oubliez donc pas d’importer le fichier (et les fonctions) dans le module contenant les tests.

Solution à l’exercice

Dans un fichier nommé, par exemple, argmax.py :

# Needed imports
from typing import List



def argmax (lst: list[int]) -> int:
    
    """
        This function returns the index of the maximum value occurring in a list of values.
        In case of an empty list, it returns -1.
        If the maximum value occurs multiple times, the index of the first occurrence is returned.
        In:
            * lst: The list of values.
        Out:
            * The index of the first occurrence of the maximum value, or -1.
    """

    # This whole function is equivalent to:
    # return lst.index(max(lst))
    # We detail the algorithm for pedagocical purpose
    # In practice, you should use the line above

    # If not found
    if lst is None or len(lst) == 0:
        return -1
    
    # Otherwise, iterate on contents to find max
    max_val = lst[0]
    max_index = 0
    for i, v in enumerate(lst):
        if v > max_val:
            max_val = v
            max_index = i
    return max_index
/**
 * This function returns the index of the maximum value occurring in a list of values.
 * In case of an empty list, it returns -1.
 * If the maximum value occurs multiple times, the index of the first occurrence is returned.
 *
 * @param lst the list of values
 * @return the index of the first occurrence of the maximum value
 */
public int argmax(int[] lst) {
    if (lst == null || lst.length == 0) {
        return -1;
    }
    int max_val = lst[0];
    int max_index = 0;
    for (int i = 1; i < lst.length; i++) {
        if (lst[i] > max_val) {
            max_val = lst[i];
            max_index = i;
        }
    }
    // Alternatively, you can use a one-liner:
    //return Arrays.stream(lst).max().stream().findFirst().getAsInt();
    return max_index;
}

Puis, dans tests_argmax.py :

# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))
        self.assertEqual(0, argmax([8, 5]))
        self.assertEqual(1, argmax([-2, 9]))

    def test_argmax_none (self):        
        self.assertEqual(-1, argmax(None))
    
    def test_argmax_empty (self):
        self.assertEqual(-1, argmax([]))
    
    def test_argmax_multiple_max (self):
        self.assertEqual(2, argmax([1, 3, 5, 5, 2, 5]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,5,2,0}));
    }

    @Test
    public void testArgmaxNone() {
        Assertions.assertEquals(-1, argmax(null));
    }

    @Test
    public void testArgmaxEmpty() {
        Assertions.assertEquals(-1, argmax(new int[]{}));
    }

    @Test
    public void testArgmaxMultipleMax() {
        Assertions.assertEquals(2, argmax(new int[]{1,3,5,5,2,5}));
    }
}

3 — Année bissextile

Dans cet exercice, vous devrez écrire une fonction is_leap(year) qui prend une année en paramètre sous forme d’entier positif, et retourne une valeur booléenne qui est vraie si l’année est bissextile et fausse sinon.

Toute année divisible exactement par quatre est une année bissextile, sauf les années divisibles exactement par 100, mais ces années séculaires sont bissextiles si elles sont divisibles exactement par 400 (Wikipedia).

Nous ajoutons une règle supplémentaire qui précise que si l’entier passé en paramètre est strictement négatif, alors la valeur retournée est False.

3.1 — Écrire les tests

Commençons par écrire un placeholder pour la fonction, qui sert juste à ce que la fonction soit définie :

def is_leap (year: int) -> bool:

    """
        The function returns True if the input is a leap year and False otherwise.
        This function only works for positive integers.
        In:
            * year: The year to be checked.
        Out:
            * True if the year is a leap year, False otherwise.
    """

    return False
/**
 * This function takes a year as input and returns True if the year is a leap year, False otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return False.
 *(assuming `is_leap` is in a file `isleap.py`)
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year){
    return false;
}

Avant de coder le corps de la fonction, vous écrirez les tests. Suivant le principe d’un test par concept, vous créerez les six tests suivants :

  1. La valeur retournée est False pour une valeur négative (donnée en exemple).
  2. Si la valeur d’entrée est impaire, la valeur retournée est False.
  3. Si la valeur d’entrée est paire et non divisible par 4, alors le résultat attendu est False.
  4. Si la valeur d’entrée est divisible par 4 et par 100, alors le résultat attendu est False.
  5. Si la valeur d’entrée est divisible par 4 et non divisible par 100, alors le résultat attendu est True.
  6. Si la valeur d’entrée est divisible par 400, alors le résultat attendu est True.

Voici un code de base pour commencer (en supposant que is_leap est dans un fichier isleap.py) :

# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)  
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {
    
    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }

}
Solution à l’exercice
# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))
    
    def test_odd_year (self):
        self.assertFalse(is_leap(1333))
        self.assertFalse(is_leap(2023))

    def test_even_not_divisivle_by_4 (self):
        self.assertFalse(is_leap(2018))
        self.assertFalse(is_leap(2026))

    def test_divisible_by_4_and_by_100 (self):
        self.assertFalse(is_leap(1900))
        self.assertFalse(is_leap(2100))

    def test_divisible_by_4_and_not_by_100 (self):
        self.assertTrue(is_leap(2008))
        self.assertTrue(is_leap(2024))

    def test_divisible_by_400 (self):
        self.assertTrue(is_leap(1600))
        self.assertTrue(is_leap(2000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)  
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {
    
    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }

    @Test
    public void testOddYear() {
        Assertions.assertFalse(isLeap(2001));
    }

    @Test
    public void testEvenYearNotDivisibleBy4() {
        Assertions.assertFalse(isLeap(2018));
        Assertions.assertFalse(isLeap(2026));
    }

    @Test
    public void testDivisibleBy4AndBy100() {
        Assertions.assertFalse(isLeap(1900));
        Assertions.assertFalse(isLeap(2100));
    }

    @Test
    public void testDivisibleBy4AndNotBy100() {
        Assertions.assertTrue(isLeap(2008));
        Assertions.assertTrue(isLeap(2024));
    }

    @Test
    public void testDivisibleBy400() {
        Assertions.assertTrue(isLeap(1600));
        Assertions.assertTrue(isLeap(2000));
    }
}

3.2 — Écrire la fonction

Vous pouvez maintenant coder la logique de la fonction conformément aux spécifications. Vous devez commenter les lignes de manière significative pour rendre le code plus clair, en utilisant #.

Important

Assurez-vous que tous les tests passent !

Solution à l’exercice
def is_leap (year: int) -> bool:
    
    """
        This function takes a year as input and returns True if the year is a leap year, False otherwise.
        An additional constraint is that the year must be greater than 0, otherwise the function will return False.
        In:
            * year: The year to be checked.
        Out:
            * True if year is a leap year, False otherwise.
    """
    
    if year >= 0:  
        if year % 4 == 0:
            if year % 100 == 0:
                return year % 400 == 0
                # since the year is divisible by 100, 
                # it must also be divisible by 400 to be a leap year     
            else:
                # divisible by 4 and not by 100
                return True
        else:
            # not divisible by 4
            return False
    else:
        # negative years are not leap years
        return False
/**
 * This function takes a year as input and returns true if the year is a leap year, false otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return false.
 *
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year) {
    if (year >= 0) {
        if (year % 4 == 0) {
            if (year % 100 == 0) {
                return year % 400 == 0;
                // since the year is divisible by 100, 
                // it must also be divisible by 400 to be a leap year     
            }else{
                // divisible by 4 and not by 100
                return true;
            }
        }else{
            // not divisible by 4
            return false;
        }
    }else{
        // negative years are not leap years
        return false;
    }
}

4 — Mot de passe valide

Pour être considéré comme suffisamment sécurisé, un mot de passe doit respecter les règles suivantes :

  1. Doit contenir au moins 8 caractères.
  2. Doit contenir au moins une lettre majuscule.
  3. Doit contenir au moins une lettre minuscule.
  4. Doit contenir au moins un chiffre.
  5. Doit contenir au moins un caractère spécial (parmi !@#$%^&*()).

Écrivez la fonction is_strong_password(password) qui prend en entrée une chaîne de caractères (a.k.a., séquence de caractères) et retourne un booléen indiquant si le mot de passe entré est fort. Pour aider à concevoir la logique de la fonction, vous pouvez la décomposer en sous-fonctions, chacune étant testée indépendamment. Un squelette de la fonction est fourni ci-dessous, divisé en sous-fonctions. Vous devez aussi documenter et commenter votre code.

Enfin, vous devez vérifier que les règles exprimées dans le cahier des charges sont bien prises en compte en écrivant des tests unitaires. La classe de test n’est pas fournie, vous devrez donc l’écrire vous-même. Chaque sous-fonction doit être testée indépendamment avec une variété d’exemples. Une fois chaque sous-fonction testée, vous pouvez tester la fonction principale.

Cette dernière étape peut être réalisée en binôme : chaque personne écrit les tests pour l’autre. Ensuite, vous pouvez évaluer la justesse de votre fonction en lançant les tests. Possiblement, soit la fonction, soit les tests devront être modifiés pour garantir que la fonction fonctionne correctement.

Voici un fichier pour vous lancer :

Solution à l’exercice

5 — Mot de passe valide à nouveau

Maintenant que vous avez une fonction qui vérifie la robustesse d’un mot de passe, vous allez la documenter en utilisant le format docstring. Vous devez documenter le module et chacune de ses fonctions avec des informations utiles, telles que le but de la fonction, les paramètres qu’elle prend, et la valeur qu’elle retourne. Ces informations aideront d’autres développeurs à comprendre comment utiliser la fonction. Des extensions dans VSCode existent pour générer automatiquement le squelette d’une docstring de fonction. Une fois que vous avez documenté le code, vous pouvez générer la documentation en utilisant l’outil pydoc ou javadoc, selon le langage que vous utilisez.

Ouvrez d’abord un terminal et naviguez jusqu’au répertoire où se trouve le fichier. Le fichier doit s’appeler strongpassword.py.

Puis, lancez la commande suivante :

python -m pydoc -w strongpassword
python -m pydoc -w strongpassword
python3 -m pydoc -w strongpassword
python3 -m pydoc -w strongpassword

pydoc générera un fichier HTML nommé strongpassword.html dans le même répertoire. Vous pouvez ouvrir ce fichier dans un navigateur web pour voir la documentation.

Ouvrez d’abord un terminal et naviguez jusqu’au répertoire où se trouve le fichier. Le fichier doit s’appeler StrongPassword.java.

Puis, lancez la commande suivante :

javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java

javadoc générera un répertoire nommé doc contenant la documentation. Vous pouvez ouvrir le fichier index.html dans un navigateur web pour voir la documentation.

Une fois fait, vérifiez la documentation générée !

6 — Optimisez vos solutions

Ce que vous pouvez faire maintenant est d’utiliser des outils d’IA tels que GitHub Copilot ou ChatGPT, soit pour générer la solution, soit pour améliorer la première solution que vous avez proposée ! Essayez cela pour tous les exercices ci-dessus, pour voir les différences avec vos solutions.

Pour aller plus loin

7 — Vérificateur Takuzu

Takuzu est un jeu de logique où vous devez compléter une grille pré-remplie $n\times n$ en utilisant uniquement des 0 et des 1. Une grille est correctement complétée si elle respecte les trois règles :

  1. Chaque ligne et chaque colonne doit contenir un nombre égal de 0 et de 1.
  2. Pas plus de deux nombres similaires consécutifs sont autorisés.
  3. Chaque paire de lignes est différente, et chaque paire de colonnes est différente.

Un exemple de grille $4\times 4$ correctement complétée :

0 1 1 0 
1 0 0 1 
0 0 1 1 
1 1 0 0 

Vous êtes invité à coder un vérificateur de grille Takuzu. Un squelette de module servira de base à votre travail. Vous devez respecter la partition des fonctions et implémenter chacune d’elles. Bien sûr, vous devrez aussi créer et compléter une classe de test.

Voici un fichier pour vous lancer :

  • TODO
Solution à l’exercice

8 — Mastermind

Préparez-vous à coder le jeu Mastermind. Dans ce jeu, un codeur crée un code utilisant 4 chiffres de 1 à 6. Un décodeur doit faire jusqu’à dix essais pour casser le code. Pour chaque essai, le codeur donne des indices au décodeur. Les indices sont composés de 2 chiffres :

  • Le premier chiffre indique le nombre de bons chiffres à la bonne position.
  • Le second chiffre indique le nombre de bons chiffres à la mauvaise position.

Les occurrences multiples d’une valeur sont autorisées. S’il y a des valeurs en double, elles ne peuvent pas toutes recevoir un indice à moins qu’elles correspondent au même nombre de couleurs dupliquées dans le code caché. Par exemple, si le code est [6, 5, 1, 4] et la proposition est [1, 1, 2, 2], les indices sont (0, 1). En fait, la valeur 1 apparaît deux fois dans la proposition, mais une seule fois dans le code. Puisqu’elle n’est pas à la bonne place, elle compte pour une dans la mauvaise place. De plus, la valeur 2 apparaît aussi deux fois dans la proposition, mais pas dans le code. Elle n’apparaît pas dans les indices.

Pour progresser progressivement, nous allons décomposer la logique du jeu en fonctions et les coder au fur et à mesure. Nous utiliserons la description fournie pour documenter le code et écrire des tests.

Nous fournissons une classe Mastermind partiellement implémentée. Des commentaires sont fournis pour toutes les méthodes présentes, qu’elles soient implémentées ou non.

Vous devrez coder les méthodes check_code(code), has_remaining_attempts(), is_right_answer(hints) et guess_pattern(code, guess). Nous recommandons de traiter une fonction après l’autre, en écrivant les tests avant ou juste après avoir codé la fonction. Comme la dernière fonction est la plus compliquée, vous pouvez, si vous le souhaitez, la décomposer en sous-fonctions, chacune testée indépendamment.

Vous pourriez avoir besoin de compter des valeurs. Les structures de données suivantes peuvent être utiles : from collections import Counter

Voici un fichier pour vous lancer :

Solution à l’exercice

Pour aller plus loin

Il semble que cette section soit vide !

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