Connexion élèves

Choisir le(s) module(s) à installer :

Tester un code et évaluer son efficacité, repérer et corriger ses erreurs.

Tests sur les entrées d'un code : les assertions

Lorsqu'on écrit un programme, on ne peut jamais prévoir toutes les situations possibles qui se produiront lorsque ce programme s’exécutera, et particulièrement toutes les valeurs possibles que pourra entrer par exemple l'utilisateur du programme et celles des arguments passés à une fonction.

Or, un code est prévu pour fonctionner normalement avec des paramètres d'un certain type, voire d'une certaine valeur bien précise !

Il est alors possible, dans l'écriture d'un code, de directement spécifier quelles préconditions doivent être respectées pour une bonne exécution du code, ce sont les assertions ( de manière générale, une assertion est une chose qui doit être considérée comme vraie. ).

Si ces préconditions ne sont pas vérifiées, alors le programme s'arrête ( il lève une exception ); au programmeur de faire alors en sorte que les préconditions soient toujours vérifiées, en rajoutant par exemple des tests sur les valeurs des paramètres avant l’exécution du code.

Prenons l'exemple d'une fonction qui calcule l'inverse d'un nombre entier positif :


	def inverse(n):
		'''
			Calcule l'inverse d'un entier positif
			
			Entrée : un entier n entier strictement positif
			Sortie : un flottant, inverse de n				
		'''
		
		return 1/n				
				

Un utilisateur "lambda" ne consultera pas forcément la docstring de la fonction ( spoiler : il ne le fera jamais... ), et il sera libre d'entrer n'importe quelle valeur, qui ne respectera donc pas forcément les préconditions ( n entier strictement positif ).

On peut alors rajouter des assertions avant l’exécution du code de la fonction :


	def inverse(n):
		'''
			Calcule l'inverse d'un nombre positif
			
			Entrée : un entier n strictement positif
			Sortie : un flottant, inverse de n				
		'''
		assert n != 0 , "Le nombre ne doit pas être nul."
		assert n > 0 , "Le nombre doit être positif."
		assert isinstance(n, int) , "Le nombre doit être un entier."
		
		return 1/n				
				

Ces assertions lèveront une exception et afficheront un message si une des préconditions n'est pas respectée :


	>>> inverse(12)
	0.08333333333333333
	
	>>> inverse(0)
	Traceback (most recent call last):
	  File "", line 1, in 
	  File "test.py", line 9, in inverse
	    assert n != 0 , "Le nombre ne doit pas être nul."
	AssertionError: Le nombre ne doit pas être nul.
	
	>>> inverse(-2)
	Traceback (most recent call last):
	  File "", line 1, in 
	  File "test.py", line 10, in inverse
	    assert n > 0 , "Le nombre doit être positif."
	AssertionError: Le nombre doit être positif.
	
	>>> inverse(1.2)
	Traceback (most recent call last):
	  File "", line 1, in 
	  File "test.py", line 11, in inverse
	    assert isinstance(n, int) , "Le nombre doit être un entier."
	AssertionError: Le nombre doit être un entier.
	
	>>> 	
			
Les assertions permettent de vérifier les préconditions d'une fonction.
La syntaxe est :

assert condition :		
		
de nombreuses possibilités existent pour la condition :
  • test avec les opérateurs classiques :
    
    assert ==, <, >, != ...		
    		
  • Appartenance à un type spécifié :
    
    assert isinstance (n, int)		
    		
  • Appartenance à une liste de valeurs
    
    assert n in [1,2,3]		
    		
Compléter les fonctions ci-dessous avec les assertions judicieuses puis testez les fonctions avec des entrées correctes et incorrectes.
  • 
    def interface():
        req = str(input("voulez vous saisir un titre de film, un acteur ou un réalisateur ? "))
        if req == "titre":
            print('appel de la fonction effectuant une requete sur un titre de film')
        elif req == "acteur":
            print('appel de la fonction effectuant une requete sur un acteur')
        elif req == "réalisateur":
            print('appel de la fonction effectuant une requete sur un réalisateur')	
    
    		
  • 
    def division_euclidienne(a, b):
    	q=a//b
    	r=a%b
    	return q,r			
    		
  • 
    def dec_vers_bin(n,bits):
        '''convertit le nombre n en binaire sur un nombre de bits'''
        binaire=""
        for a in range(bits):
            binaire=str(n%2)+binaire
            n=n//2
        return binaire	    	
    		
Dans certains cas les assertions peuvent être détournées de leur utilisation initiale pour réaliser des tests unitaires. Dans ce cas ils ne vérifient plus les préconditions de la fonction mais ses posts-conditions.
Les assertions sont alors regroupées dans une fonction de test.
Par exemple si on veut tester la fonction somme() ci-dessous :

def somme(a,b):
    return a+b
		
On peut imaginer la fonction de test :

def test_somme():
    assert somme(0,0)==0
    assert somme(1,1)==2
    assert somme(1,-1)==0
    assert somme(12,3)==15
    assert somme(12,-3)==9
    assert somme(50,100)==150
		
Cet usage est un détournement de la fonction assert.
Nous allons voir maintenant comment réaliser de vrais tests unitaires

Tests du résultat d'une fonction : les tests unitaires

La notion de tests est fondamentale en informatique : certains systèmes ne peuvent se contenter d'un fonctionnement approximatif mais doivent au contraire être robustes, c'est à dire fonctionner correctement dans toutes les situations possibles.

Pour prouver qu'une fonction fait toujours correctement le travail pour lequel elle est prévue, il faudrait théoriquement la tester avec tous les arguments possibles et imaginables; c'est bien entendu impossible...

On peut cependant se contenter de tester son bon fonctionnement sur quelques arguments bien choisis : on parle alors de tests unitaires

En Python, on peut utiliser le module doctest pour réaliser ces tests unitaires; il permet d'indiquer dans la docstring de la fonction des tests à réaliser et le résultat attendu si elle fonctionne bien.
Si ce n'est pas le cas, un message est alors affiché signalant qu'il faut corriger son code !

Voici un exemple avec une fonction qui donne le quotient et le reste de deux entiers :


	import doctest
	
	def division( n1, n2 ):
	    """
	    	Fonction pour calculer le quotient et le reste de la division de deux nombres.
	
	    	Entrée :
	        deux nombres n1, n2
	    	Sortie :
	        deux entiers dans l'ordre : quotient, reste de la division de n1 par n2
	
	    Tests unitaires :
	    >>> division(1,1)
	    (1, 0)
	
	    >>> division(2,1)
	    (2, 0)
	
	    >>> division(10,3)
	    (3, 1)
	    """
	
	    quotient = n1 // n2
	    reste = n1 % n2
	    return quotient, reste
	
	# Programme principal
	
	doctest.testmod()	# exécution des test unitaires		
			

Dans la docstring, un test unitaire correspond aux 3 chevrons (>>> ) suivis de l'appel de la fonction avec des arguments particuliers; on indique en dessous le résultat attendu.

Si les tests unitaires sont validés, rien ne s'affiche. Par contre, en cas de mauvais fonctionnement, un ou plusieurs avertissement(s) s'affichent indiquant quel(s) test(s) n'ont pas été réussi(s).
Exemple avec la fonction précédente buguée :


	import doctest
	
	def division( n1, n2 ):
	    """
	    Fonction pour calculer le quotient et le reste de la division de deux nombres.
	
	    Pré :
	        deux nombres n1, n2
	    Post :
	        deux entiers dans l'ordre : quotient, reste de la division de n1 par n2
	
	    Tests unitaires :
	    >>> division(1,1)
	    (1, 0)
	
	    >>> division(2,1)
	    (2, 0)
	
	    >>> division(10,3)
	    (3, 1)
	    """
	
	    quotient = n1 / n2  # LA FONCTION EST BUGUÉE !!!
	    reste = n1 % n2
	    return quotient, reste
	
	# Programme principal
	
	doctest.testmod()	# exécution des test unitaires		
			

*********************************************************************
File "test.py", line 14, in __main__.division
Failed example:
    division(1,1)
Expected:
    (1, 0)
Got:
    (1.0, 0)
**********************************************************************
File "test.py", line 17, in __main__.division
Failed example:
    division(2,1)
Expected:
    (2, 0)
Got:
    (2.0, 0)
**********************************************************************
File "test.py", line 20, in __main__.division
Failed example:
    division(10,3)
Expected:
    (3, 1)
Got:
    (3.3333333333333335, 1)
**********************************************************************
1 items had failures:
   3 of   3 in __main__.division
***Test Failed*** 3 failures.

>>> 	
			

La fonction renvoyant un flottant alors que c'est un entier qui est attendu, une erreur est signalée.

Une méthode de développement appelé TDD préconise d'ailleurs d'écrire D'ABORD des tests avant même le code d'une fonction...

Sur le modèle précédent, prévoir des test unitaires pour les deux fonction ci-dessous.
  • 
    def somme (N):
    	somme = 0
    	for i in range(1, N+1):
    		somme = somme + i
    	return somme
    		
  • 
    def dec_vers_bin(n,bits):
        '''convertit le nombre n en binaire sur un nombre de bits'''
        binaire=""
        for a in range(bits):
            binaire=str(n%2)+binaire
            n=n//2
        return binaire			
    		
All unit tests passed

Rechercher et analyser les erreurs d'un code

Analyse des messages d'erreur de la console

Lorsqu'un programme ne s'exécute pas totalement, la console python nous fourni un message d'erreur.
L'analyse de ces message (en anglais...) nous permet souvent de gagner beaucoup de temps (recherche fastidieuse d'une parenthèse, de deux points manquants).
Il faut cependant savoir et comprendre ces messages.

Les petites erreurs à éviter

Dans le code ci-dessous vous voyez sûrement l'erreur évidente de synthaxe

L=['toto',2,1.23,True
a=1
					
Cependant, la console python nous indiquera dans ce cas le message suivant :

  File "test.py", line 2
    a=1
    ^
SyntaxError: invalid syntax			
			
Il semble difficile de déceler une erreur à la ligne indiquée par la console python (pas d'erreur de syntaxe dans a=1 )
Dans ce cas il faut chercher avant l'indication de la console.
En effet l'interpréteur de python attend le crochet fermant de la liste L et bute sur le signe =.
Si une ligne signalée comme fausse semble correcte, toujours regarder la fin de la ligne précédente.

Décodage des messages d'erreur

Pour une fois vous devez faire des erreurs !
Vous devez produire un code pour chaque message d'erreur proposé ci-dessous. Votre code devra générer le message proposé.

1. IndexError: list index out of range

2. SyntaxError: invalid syntax

3. TypeError: 'int' object is not subscriptable

4. NameError: name 'variable' is not defined

5. SyntaxError: EOL while scanning string literal

6. TypeError: unsupported operand type(s) for /: 'str' and 'str'

7. ValueError: invalid literal for int() with base 10: '12.5'

8. TypeError: object of type 'float' has no len()

9. IndentationError: unexpected indent

10. IndentationError: expected an indented block

11. IndentationError: unindent does not match any outer indentation level

12. TypeError: f1() takes 0 positional arguments but 1 was given

13. TypeError: f2() takes 1 positional argument but 2 were given

14. ValueError: invalid literal for int() with base 10: '12.5'		
			

Affichage de la trace des variables

Il peut arriver qu'un programme s'exécute normalement mais ne réalise pas la tache demandée. Il est alors nécessaire de trouver la ou les erreurs algorithmiques présentes dans le code...
Malheureusement, la plupart du temps, regarder le code "dans les yeux" ne suffit pas.il faut donc :
  1. Relire une fois attentivement son code en lien avec le déroulement de l'algorithme prévu. Si cela ne vous a pas permis de résoudre tous les problèmes...
  2. Identifier les variables cruciales de votre code (pas plus de 2 ou 3).
  3. Afficher ces variables grâce à la commande print en précisant à chaque fois quelle variable est affichée (print('i=',i) plutôt que print(i) et en commentant cet affichage de test(#test)
  4. Analyser ces affichages et recommencer à la première étape
Tenter de debuguer ce programme en utilisant la méthode ci-dessus.
				
from random import randint

nombre = randint( 1 , 100
entree = 0



while ( entree == nombre ) :

    entree = int(input('Entrez votre proposition :'))
    if ( entree > nombre )) :
        print('Trop grand !')
    elif ( entree < nombre ) 
        print('Trop petit !')

    print('Bravo !')		
		

Évaluer l'efficacité

Si on veut par exemple comparer l'efficacité de deux programmes différents, on peut vouloir mesurer leur temps d'exécution.

Un module existe pour cela, le module timeit. Par exemple, pour mesurer la durée d’exécution d'une fonction :


	from timeit import timeit

	def test(n):
	    """Fonction inutile"""
	    L = [i for i in range(n)]
	
	t = timeit(stmt = 'test(1000)', globals=globals(), number = 100)
	print(t)
			

Le module mesure la durée moyenne ( en secondes ) de l’exécution de la fonction test() sur plusieurs exécutions successives ( cette durée est souvent influencée par de nombreux paramètres extérieurs au code ).

Debug