Accueil

Traduction

Tutoriel Python - sommaire

Tutoriel Python - recherche

L'auteur : Patrick Darcheville

Vous pouvez me contacter via Facebook pour questions & suggestions : Page Facebook relative à mon site

Python : des programmes plus professionnels - gestion des exception


Dans ce chapitre nous allons aborder des notions très importantes en programmatique pour produire des programmes "robustes" c'est à dire des programmes qui peuvent être proposés aux utilisateurs.

Un programme structuré

Dès qu'un programme correspond à un algorithme un peu plus complexe que ce que nous avons vu jusqu'à présent, il est préférable de le structurer.

Le programme principal est relativement court car on se contente d'importer les modules nécessaires, de saisir des variable, d'appeler des sous-programmes (SP).
Un même sous-programme peut être appelé plusieurs fois par le programme principal.

Avec Python cette distinction n'a pas d'intérêt pratique : les procédures et fonctions sont définies avec le même mot clé  : def.
Dans le cas d'une fonction il y aura une instruction return ; instruction qui sera absente d'une simple procédure.

Premier programme structuré

Objectif :

Le programme

# nom du programme : cercle.py
#objet du programme : programme structuré sur le cercle

#procédures et fonctions ----------------------
def cercle1(r):
    c = rayon *2 * math.pi
    s = rayon**2 * math.pi
    return c, s

def cercle2(r):
    pencolor('navy')
    pensize(3)
    speed("slow")
    circle(r)
    exitonclick()
#------------------------------------------------
#programme principal
import math
from turtle import *
rayon = input("saisir le rayon (un entier) : ")
rayon =eval(rayon) #conversion chaine en numérique
circonference, aire = cercle1(rayon)    # appel fonction cercle1
print ("circonférence du cercle : " , circonference)
print ("aire du cercle : " , aire)
cercle2(rayon)  #appel procédure cercle2

Description de la fonction "cercle1"

J'utilise la fonction pi du module math donc je dois importer ce module.
Les variables définies dans cette fonction sont : r, c, s
Cette fonction retourne un tuple de deux valeurs.

Description de la procédure "cercle2"

La variable utilisée dans cette procédure est r.
Cette variable est utilisée pour dessiner le cercle avec le module turtle.

Description du programme principal

Importation de deux modules.
Saisie du rayon.
Appel de la fonction "cercle1" avec passage en argument du rayon.
Les deux valeurs retournées par la fonction "cercle1" sont récupérées dans le programme principal respectivement dans les variables "circonference & aire".
Affichage de ces deux variables.

Appel de la procédure "cercle2" avec passage en argument du rayon.

Batterie de tests

Vous avez saisi le code source et vous vous êtes assuré qu'il fonctionne correctement.
Au fait, n'oubliez pas de cliquer dans la fenêtre graphique pour "reprendre la main".

Maintenant vous allez découvrir via des tests, la notion de portée des variables ainsi que la problèmétique des erreurs de saisie.

Portée des variables

Premier test : dans les deux SP ("cercle1" & "cercle2") rajoutez l'instruction "print(rayon)" ; exécutez de nouveau le programme !
Pas de plantage : le rayon est affiché deux fois.

Deuxième test : dans le programme principal rajoutez " print(c,s)" puis lancez l'exécution.
Il y a plantage !

...
NameError: name 'c' is not defined

Tout cela est normal.
Une variable définie au niveau d'un SP (procédure ou fonction) n'est connue que de ce bloc d'instructions ; elle ne peut être manipulée dans le programme principal ; on dit qu'elle est locale.
Par contre une variable définie au niveau du programme principal peut être manipulée également dans les SP ; Cette variable a donc une portée plus grande ; on dit qu'elle est globale.

Erreur de saisie

Lancez l'exécution du programme et saisissez la chaine "100 cm" . Il y a "plantage" !
La fonction eval() est incapable de convertir en numérique une chaine comprenant des chiffres mais aussi des lettres.

C'est la faute de l'utilisateur qui n'a pas lu l'invite de commande : un entier !
Mais un bon programme doit prévoir les étourderies de l'utilisateur. Il faut donc améliorer ce programme et prévoir un contrôle de saisie.
C'est l'occasion d'introduire une notion très puissante mais parfois un peu délicate : les expressions régulières.
On retrouve ce concept dans tous les langages.

Le programme "cercle_plus.py"

Il s'agit d'une amélioration de "cercle.py".
Les fonctions sont identiques à la version précédente.

Ce programma autorise la saisie d'un nombre décimal.

Le code du programme principal dans la nouvelle version

#programme principal
import math
import re
from turtle import *
format = "^[0-9\.]{1,}$"
rayon =""
while not re.search(format,rayon): 
    rayon = input("saisir le rayon sous forme d'un nombre entier ou décimal  : ")
rayon =eval(rayon)      #conversion chaine en numérique
circonference, aire = cercle1(rayon)    # appel fonction cercle1
print ("circonférence du cercle : " , circonference)
print ("aire du cercle : " , aire)
cercle2(rayon)  #appel procédure cercle2
...

Commentaire du code

Whoua ! ça se complique sérieusement.
Voyons toutes les nouveautés :
import re : activation du module re (Regular Expressions) donc le module qui traite des expressions régulières
format ="^[0-9\.]{1,}$" : la variable format contient une expression régulière ; celle-ci précise le format de saisie autorisé.
while not re.search(format, rayon) : tant que le contenu de rayon ne correspond pas au format de saisie défini dans format on reste dans la boucle de saisie.
Nous avons donc utilisé la méthode search() du module re.

Mais que signifie ^[0-9\.]{1,}$ ; c'est ressemble à du chinois !

Exécution du script

saisir le rayon sous forme d'un nombre entier ou décimal  : 12cm
saisir le rayon sous forme d'un nombre entier ou décimal  : 12,5
saisir le rayon sous forme d'un nombre entier ou décimal  : 12.5
circonférence du cercle :  78.53981633974483
aire du cercle :  490.8738521234052

Seuls les chiffres et le point sont autorisés par l'expression régulière (RE)
Donc si la chaine saisie contient d'autres caractères, on reste dans la boucle de saisie.

À propos des expressions régulières

Je n'en dit pas davantage sur les expressions régulières car les RE sont traitées dans le chapitre XV.
Le point sur les différentes conteneurs - les RE

Gestion des exceptions

En Python, comme dans tous les langages, le terme "exception" désigne une erreur lié à l'environnement web.
Il peut s'agir d'une coupure de connexion internet, de l'usage d'un navigateur obsolète mais le plus souvent d'une erreur de saisie de l'utilisateur.

Dans le programme "cercle_plus.py" j'ai prévu une boucle de saisie ; tant que la saisie ne correspond à la RE on reste dans cette boucle.
Cependant cette solution n'est pas complètement satisfaisante : l'utilisateur peut recommencer la saisie mais il ne sait pas la nature de son erreur ...
Python, comme les autres langages, propose une structure pour ce qu'on appelle la gestion des exceptions.
Le thème pour illustrer cette technique sera la division par zéro ou par quelque chose qui n'est pas numérique.

Script sans gestion des erreurs

Le code

reponse ="o"
while reponse in "oO":
        D = input("Dividende  : ")
        d = input("Diviseur : ")
        D =float(D)
        d =float(d)
        print("Division normale, le quotient est : ", D/d)
        print("Division entière , le quotient est : ", D//d)
        print("Division entière, le reste est : ", D%d)
        
        reponse =input("encore des  saisies o / n ? : ")

Le rendu

Dividende  : 10
Diviseur : 3
Division normale, le quotient est :  3.3333333333333335
Division entière , le quotient est :  3.0
Division entière, le reste est :  1.0
encore des  saisies o / n ? : o
Dividende  : 10
Diviseur : 0
...
ZeroDivisionError: float division by zero

Si je saisis 0 en guise de diviseur le programme "plante" .

Script avec gestion élégante des erreurs

Le code

La solution est d'utiliser la structure try ... except ... finally ...

reponse ="o"
while reponse  not in "nN":
    try: 
        D = input("Dividende  : ")
        d = input("Diviseur : ")
        D =float(D)
        d =float(d)
        print("Division normale, le quotient est : ", D/d)
        print("Division entière , le quotient est : ", D//d)
        print("Division entière, le reste est : ", D%d)
    except Exception as e : 
        print(e)
    finally : 
        reponse =input("encore des  saisies o / n ? : ")

Si erreur de saisie les traitements ne sont pas effectués et branchement sur le bloc "except".
Qu'il y ait erreur de saisie ou pas , le bloc "finally" est toujours exécuté.

Le rendu

Dividende  : 10
Diviseur : o
could not convert string to float: 'o'
encore des  saisies o / n ? : o
Dividende  : 10
Diviseur : 0
float division by zero
encore des  saisies o / n ? : O
Dividende  : 10
Diviseur : 3
Division normale, le quotient est :  3.3333333333333335
Division entière , le quotient est :  3.0
Division entière, le reste est :  1.0
encore des  saisies o / n ? : o 
Dividende  : 1O
Diviseur : 2
could not convert string to float: '1O'
encore des  saisies o / n ? : n

Même si je saisis en guise de dividende/diviseur une lettre ou zéro, le programme ne plante plus et de plus je suis informé de la nature de l'erreur de saisie : 'could not convert string to float', 'division by zero'.
L'erreur peut être détectée en amont des divisions. Ainsi en cas de saisie d'une lettre en guise de dividende / diviseur c'est lors de la tentative de conversion que l'erreur est détectée.

Formater les affichages

Les chaines de format ou "f-strings" permettent d'afficher les sorties selon un format approprié. Il suffit à l'intérieur du print() d'un f majuscule ou minuscule juste après la parenthèse ouvrante.

Thématique : calcul du montant TTC à partir du montant HT

Programme basique

#nom du programme : tva.py
#objet : calcul tva et ttc à partir du ht

ht = input("saisir le montant HT: ")
ht =eval(ht)

print("TVA : ", ht * 0.1)
print("TTC : ", ht * 1.1)

Si vous saisissez 1000, ce programme retourne :
TVA : 100
TTC : 1100

Un programme amélioré

#nom du programme : tva_plus.py
#objet : calcul tva et ttc à partir du ht avec contrôles des saisie

import re
format = "^[0-9\.]{1,}$"
ht =""
taux =""
while not re.search(format,ht): 
    ht = input("saisir le montant HT (entier ou décimal) :")
while  not re.search(format,taux):
    taux = input("saisir le taux de tva pour 100€ de HT (10 ou 20) : ")
ht =eval(ht)
taux =eval(taux)

print(f"TVA : {ht * taux /100:.2f} €")
print(f"TTC : {ht * (1 + (taux/100)) :.2f} €")

Ce programme calcule la TVA et le TTC au taux réduit ou au taux normal. De plus il y a des contrôles de saisie : vous pouvez faire plusieurs tentatives pour la saisie du montant HT et du taux de TVA sans 'plantage' du script.

On associe aux instructions print des "f-strings". Les montants affichés sont libellés en € !
Notez bien la syntaxe à l'intérieur des parenthèses de la fonction print : f"TVA : {ht * taux /100:.2f} €".
Dans la chaine il y peut y avoir une expression entre accolades ; notation "moustache".
Cette expression comprend le calcul (ht * taux/100) suivi du format d'affichage (:.2f) ce qui veut dire avec deux décimales.

Trace d'exécution du programme

saisir le montant HT (entier ou décimal) :1000 €
saisir le montant HT (entier ou décimal) :1000,50
saisir le montant HT (entier ou décimal) :1000.5
saisir le taux de tva pour 100€ de HT (10 ou 20) : 20
TVA : 200.10 €
TTC : 1200.60 €

Tirage au sort de cinq cartes : version définitive

Je reviens une nouvelle fois sur le thème "tirage au sort de cinq cartes dans un jeu de 32". Dans le chapitre 8, ce programme avait déjà été amélioré avec l'emploi d'un "set" interdisant les doublons.
Dans la version définitive, le programme est structuré avec la création d'une fonction "tirage_carte()" afin d'avoir un code beaucoup plus lisible et donc plus facile à maintenir.

Le code

# nom programme : tirage_carte_fonction.py
#objet : tirage au sort de 5 cartes.
""" Version améliorée :
les cartes tirées sont rangées dans un conteneur de type set/
Ainsi il ne peut y avoir de doublons.
"""
# sous-programmes 
def tirage_carte():
        carte_e = choice (enseignes)
        carte_r = choice (rang)
        carte_tiree = str(carte_r) + " de " + carte_e
        return carte_tiree

#programme principal
from random import *
enseignes = ['pique', 'coeur', 'carreau', 'trèfle']
rang = ['As', 'Roi', 'Dame', 'Valet', 10, 9, 8 , 7]
reponse ="o"
while reponse in "Oo":
        mon_jeu =set()      # la main stockée dans un ensemble
        n = 0
        while n < 5:
                carte = tirage_carte()      #appel fonction 
                mon_jeu.add(carte)
                n = len(mon_jeu)
        print("Mon jeu : ")
        for carte in mon_jeu:
                print(carte, end =" - ")
        print("\n ----------------------------------------------")
        reponse =input("Encore un tirage o/n  ? : " ) 

Tant que n (nombre d'éléments du set "mon_jeu") est inférieur à 5, il faut boucler.
carte = tirage_carte() : la variable globale "carte" récupère la valeur retournée par la fonction.

Le rendu

Mon jeu : 
As de coeur - Dame de trèfle - 7 de coeur - 9 de trèfle - Valet de coeur - 
 ----------------------------------------------
Encore un tirage o/n  ? : o
Mon jeu : 
10 de pique - 8 de pique - Dame de coeur - Roi de pique - 9 de coeur - 
 ----------------------------------------------
Encore un tirage o/n  ? : o
Mon jeu : 
As de coeur - 7 de pique - 9 de carreau - 8 de carreau - Valet de coeur - 
 ----------------------------------------------
Encore un tirage o/n  ? : n