© Your Copyright
L’objectif de ce cours est d’acquérir un certain nombre de bonnes pratiques concernant la programmation procédurale. Il s’agit principalement de comprendre le principe de la barrière d’abstraction. Pour valider cette objectif il faudra être capable d’écrire correctement le module correspondant à un type abstrait de donnée.
Nous nous appuyons sur le paradigme de programmation procédurale. Il est impensable de réaliser le projet avec un code purement séquentiel sans le structurer en de nombreuses fonctions. Comme prérequis à ce cours, vous devez donc maîtriser parfaitement ce qu’est une fonction, comment on la définit, comment on lui passe des paramètres, comment elle renvoie une valeur de retour, quelle est la portée de ses variables locales.
python : code_run132.pySorties
Découper un problème en sous problèmes de plus en plus simples jusqu’à ce que tout soit facilement manipulable, correspond à la méthode réductionniste très largement utilisée en sciences. Vous savez décomposer un programme en fonctions/procédures. Il s’agit alors de découper le programme jusqu’à ce qu’il ne soit plus composé que de fonctions simples à comprendre, écrire, modifier ou corriger. Dites vous que si le code d’une fonction est trop long pour être entièrement affiché à l’écran, il y a de forte chance pour que cela vaille la peine de la décomposer en sous fonctions. Enfin, le fait qu’une fonction puisse être appelée plusieurs fois dans différents contextes permet la factorisation du code. On évite ainsi la redondance de code.
Lorsqu’on écrit une fonction on procède par étapes. Cela permet d’avancer de manière cohérente dans le codage et de pouvoir s’interrompre pour reprendre plus tard.
- Prototype:
D’abord on écrit le prototype de la fonction. C’est à dire : son nom, ce qu’elle prend en paramètre et ce qu’elle renvoie (rien dans cet exemple ci-après). Le nom d’une fonction doit être un verbe (
do()
) ou un groupe verbale (doIt()
) cohérent avec l’action réalisée par la fonction.def show(grid): return
- Commentaires:
Ensuite à l’aide de commentaires on peut indiquer le but de la fonction puis la trame de l’algorithme à réaliser. Cela permet de réfléchir à ce qu’on va faire avant de se lancer bille en tête dans le code.
def show(grid): #affiche une image contenue dans grid #effacer la console #pour chaque case de la grille #si il y a un caractere a afficher #deplacer le curseur au bon endroit #ecrire caractere #ecrire un saut de ligne return
- Structures de contrôle (for, if...) et appels des sous fonctions
Puis, on commence à écrire les structures de contrôle, les conditions si elle sont simples puis on commence à indenter le code.
def show(grid): #effacer la console clearTerm() #pour chaque case de la grille for i in range(len(grid)): for j in range(len(grid[i])) : #si il y a un caractere a afficher if(): #deplacer le curseur au bon endroit moveCursor() #ecrire caractere write(c) #ecrire un saut de ligne write('\n') return
- Codage des détails:
On finalise le code.
def show(grid): #effacer la console clearTerminal() #pour chaque case de la grille for i in range(len(grid)): for j in range(len(grid[i])) : c=grid[i][j] #si il y a un caractere a afficher if(not (c==None or c=='')): #deplacer le curseur au bon endroit moveCursor(i,j) #ecrire caractere stdout.write(c) #ecrire un saut de ligne stdout.write('\n') return
Tests
Voir la section sur les tests.
Afin de bien comprendre le fonctionnement d’un programme il est possible de représenter les appels successifs des fonctions qui auront lieu lors de son exécution. Ces appels se représentent facilement sous la forme d’un arbre. Ainsi le code suivant:
def a(x):
y=c(x)
z=b(y)
retun z
def b(x):
return c(x)+1
def c(x):
return -x
a(12)
donne l’arbre d’appels suivant:
Les arbres d’appels de fonctions vous serviront lors de la phase de conception du projet pour imaginer et décrire le fonctionnement du logiciel avant d’avoir écrit le code.
Dessiner l’arbre d’appels de fonctions de la fonction principale main()
.
def f1(): f2() f3() def f2(): pass def f3(): f2() def main(): f1() def f5(): pass main()
Dessinez sur une feuille de papier.
Un bon codeur est un bon fainéant! Il veut absolument éviter de faire plusieurs fois le même travail pour rien. Pour atteindre cet objectif, il doit activement chasser les zone de code redondant.
La redondance de code, c’est quand on programme plusieurs fois la même séquence d’instruction ou qu’on définit plusieurs fois la même valeur dans un programme. Qui dit code redondant, dit au moins deux fois plus d’opérations pour écrire, maintenir et modifier le programme. La redondance de code introduit de sérieuses complications lors de la phase de débogage. La volonté d’optimiser les temps de calculs peut conduire à produire du code redondant, mais cela ne nous concerne pas ce semestre. Lorsque qu’on fait du copier/coller de code il est bon de se demander si on est pas en train d’introduire de la redondance. Ayez bien conscience qu’il n’y a aucun mal à programmer des fonctions très élémentaires. Ce n’est pas un problème d’avoir une fonction d’une seul ligne et ce n’est pas un problème d’avoir une multitude de fonctions dans un programme si cela permet d’éviter la redondance et de simplifier le code.
Les exercices/exemples suivants vous présentent 4 types de redondance:
Pouvez-vous modifier le code pour éliminer les parties redondantes?
Ne jamais écrire plusieurs fois la même valeur “en dure” dans le code. Privilégier l’utilisation d’une variable.
l=[]
sizeL=10
#initialisation de la liste
for i in range(sizeL):
l.append(i)
#modification de la liste
for j in range(sizeL):
l[j]=l[j]+l[(j+1)%10]
print l[j]
Pouvez-vous modifier le code pour éliminer les parties redondantes?
Utiliser des listes plutôt que des variables avec indices
def createGuardian():
return {'position': (0,0) }
guardians=[]
#creation des gardes
for i in range(3):
guardians.append(createGuardian())
#initialisation de leur positions
for g in guardians:
setRandomPosition(g)
Pouvez-vous modifier le code pour éliminer la redondance?
Quand deux fonctions ont des sections de code identiques, il peut être utile de factoriser ce code au moyen d’une troisième fonction.
#Code factorise
def movePlayer(player,dt):
print ("Le joueur se déplace")
move(player,dt)
def moveMonster(monster,dt):
msg=monster["name"]+" se déplace."
print (msg)
move(monster,dt)
def move(a,dt):
vx,vy=a["speed"]
x,y=a["position"]
x=x+vx*dt
y=y+vy*dt
a["position"]=x,y
Pouvez vous modifier le code pour éliminer la redondance?
Quand on développe du code on évite de réinventer la roue. Il faut consulter la documentation pour et utiliser les fonctions natives du langage si elles existent.
liste=['a','b','c','d']
#l operation index de python peut etre utilisee
print liste.index('b')
Un bug ou bogue, est une erreur de conception ou de codage qui conduit au dysfonctionnement d’un programme informatique. Aucun développeur, même très expérimenté, ne peut coder sans faire d’erreur. Souvent, la majeur partie du temps de développement est consacré à l’identification et à la correction des bugs. Voici une liste non exhaustive de bugs que vous rencontrerez:
- Faute de frappe
- Fuite mémoire
- Erreur de condition d’arrêt d’une boucle (boucle infinie)
- Division par 0
- Dépassements de capacité d’une séquence
- Dépassement d’un entier
- Erreur d’initialisation d’une variable
- Erreur dans le passage de paramètres
- Interblocage
La recherche et la correction de bugs occupera la majeur partie de votre temps de développement lors du projet. Le meilleur moyen de ne pas passer trop de temps à déboguer, c’est d’éviter d’introduire des erreurs ou de les détecter très vites, au moment du développement de la section de code concernée. La rigueur que vous mettrez dans le respect des méthodes de développement, des règles d’écriture de code et dans les tests sera cruciale.
Les tests correspondent à la partie ascendante du cycle de développement en V.
Il existe plusieurs types de tests:
- Tests unitaires : Sur partie d’un programme pour valider les fonctions indépendamment.
- Tests d’integration : Lorsqu’on associe des parties de code déjà testées indépendamment.
- Tests de validation : On teste l’adéquation entre les spécifcations du document de conception et le logiciel réalisé.
- Recette : Acceptation du logiciel par la maîtrise d’œuvre
- Tests de non régression : Permet de rejouer tous les tests à la suite d’un correctif ou d’une évolution.
Tester un module Python:
#code a tester
def movePlayer(player,dt):
print ("Le joueur se déplace")
move(player,dt)
def moveMonster(monster,dt):
msg=monster["name"]+" se déplace."
print (msg)
move(monster,dt)
def move(a,dt):
vx,vy=a["speed"]
x,y=a["position"]
x=x+vx*dt
y=y+vy*dt
a["position"]=x,y
#Test
if __name__ == '__main__':
#test move
a={"speed":(6,2),"position":(3,4)}
move(a,0.5)
if(a[position]!=(6,5)):
print("move() test error")
#test movePlayer
movePlayer(a,0.5)
if(a[position]!=(6,5)):
print("movePlayer() error")
#test moveMonster
a["name"]="bob"
moveMonster(a,0.5)
if(a[position]!=(6,5)):
print("moveMonster() error")
#Level.py
def create(width=5, height=5):
level={'height':height,'width':width}
level['grid']=[[' ' for x in xrange(height)] for x in xrange(width)]
return level
def addWall(l,x,y):
l['grid'][x][y]='*'
def isEmpty(l,x,y):
#teste si il y a un mur dans la case
if l['grid'][x][y]==' ':
return True
else:
return False
if __name__=='__main__':
level=create(5,5)
if isEmpty(level,3,2)==False:
print 'la case devrait etre vide'
else:
addWall(level,3,2)
if isEmpty(level,3,2):
print 'la case ne devrait pas etre vide'
else:
print 'Level.py test ok'
En python, un module est un fichier qui contient un programme python.
Le nom du fichier est le nom du module suivi de l’extension .py
.
Quand la taille d’un programme devient conséquente, il est nécessaire de découper ce programme en plusieurs modules pour les raisons suivantes:
- Lisibilité: En regroupant certaines parties de code dans des modules cohérents, il est plus facile de lire et donc de comprendre un programme.
- Modularité: Notamment lorsqu’on travail en équipe, il est intéressant de concevoir des parties de programme plus ou moins indépendantes qu’on peut développer, modifier et remplacer plus facilement.
- Ré-utilisabilité: un module bien fait peut devenir une bibliothèque qu’on pourra réutiliser à l’occasion de futur projets.
Les modules s’importent les uns les autres en utilisant l’instruction import module
Lorsqu’un module A
importe un module B
, le module A
peut utiliser les fonctions de B
. En contrepartie, A
devient dépendant de B
. En effet, si B
est modifié ou supprimé, cela peut entraîner un dysfonctionnement du module A
.
Pour bien comprendre les relations de dépendance entre les modules d’un programme, il est nécessaire de savoir dessiner un graphe de dépendances entre les modules. Ici, un programme principal importe 4 modules :
Lors de la phase de conception d’un logiciel on cherchera à imaginer le découpage du logiciel en modules les plus indépendants possible, en évitant les références circulaires.
Le graphe ci-dessus illustre des dépendances probablement mal conçues entre différents modules.
Reprendre l’exemple de code donné au début de ce cours pour un produire un graphe de dépendances entres ses différents modules. ExempleAnimat.tgz ou ExempleAnimat.zip
Le découpage d’un programme en modules ne doit pas se faire de façon aléatoire. Il faut bien y réfléchir, car cela définit l’architecture du projet. Le principe architecturale qui vous est imposé pour le projet, repose sur le principe de la mise en œuvre de barrière d’abstraction et l’utilisation de types abstraits de données.
Le principe de la mise en œuvre de la barrière d’abstraction repose sur l’identification de types abstraits de données et permet le découpage du logiciel en modules indépendants. Ce principe n’est absolument pas imposé par le langage python, il s’agit d’un élément de méthodologie. Une fois ce découpage réalisé, la répartition des fonctions à travers les différents modules devient logique, favorisant ainsi l’homogénéité du code et le travail en équipe.
La barrière d’abstraction permet de cacher dans un module les détails d’implémentation des services rendus par ce module. Pour utiliser une fonction, il n’y a pas besoin de savoir comment elle est codée. Il suffit de connaître les spécifications qui fournissent une définition abstraite du service rendu. De plus, pour manipuler des données complexes à l’aide de fonctions, il n’est pas toujours nécessaire de savoir comment ces données sont structurées. C’est là qu’intervient la notion de type abstrait.
ball= {'position':(5.0,0.0), 'speed':(0.0,0.0), 'radius'='1', 'state':'normal' }
.
bonusBall= {'position':(0.0,0.0), 'speed':(0.0,1.0), 'radius'='1', 'state':'fast' }
.
Nous pouvons décider de créer un nouveau type de données:Ball
; en indiquant qu’une donnée de ce type est un dictionnaire comprenant des entrées pour les clés suivantes :'position'
,'speed'
,'radius'
et'state'
. La position et la vitesse sont destuple
de 2 réels. Le rayon est un entier. L’état est une chaîne de caractère.
2. Abstrait : On qualifie les types d’abstraits car on veut pouvoir manipuler les données sans se préoccuper de la manière dont elle sont codées. Par exemple, lorsque nous manipulons une donnée de type Ball
avec une fonction move(ball)
, peu nous importe de savoir si la balle est une liste, un dictionnaire ou autre chose, tant que la fonction move
qui prend en paramètre une donnée de type Ball
remplit sont rôle et modifie la position de la balle.
Dans cet exemple, “abstrait” signifie donc que je sais que je manipule une structure de type Ball
; que je sais que cette structure contient des informations relatives à sa vitesse, sa position, son état et son rayon; mais que je ne sais pas comment elle est construite dans le détail:
Type : Ball = struct
position : float,float
speed : float,float
radius : int
state : str
Attention la définition du type ci-dessus n’est pas du code python. Il s’agit de ce qu’on souhaite trouver dans le document de conception. Il faudra être suffisamment rigoureux pour coder en accord avec ce qui aura été conçu, en respectant notamment scupuleusement le nommage des données.
Pour mettre en œuvre la barrière d’abstraction, il faudra être capable d’identifier des types de données. Puis, il suffira de créer un module par type de données. L’exemple suivant montre comment créer un module à partir des données utilisées pour créer des graphes de ville.
Type : CityInfo = struct
name : str
population : int
Pour construire le module correspondant au type cityInfo
:
Regrouper dans un module toutes les fonctions qui manipulent la même donnée :
CityInfo.py
. Déclarer le typeCityInfo
class CityInfo : pass
- Constructeur: Définir une fonction pour créer la donnée:
def create(name='',population=0): cityInfo=CityInfo() cityInfo.name=name cityInfo.population=population return cityInfo
- Accesseurs: Proposer des accesseurs pour retrouver des informations décrites par la donnée. On utilise le mot clé
get
: def get_name(cityInfo): return cityInfo.name def get_population(cityInfo): return cityInfo.population
- Mutateurs: Proposer des mutateurs pour modifier la donnée. On utilise le mot clé
set
. def set_name(cityInfo, name): cityInfo.name=name def set_population(cityInfo,number): cityInfo.population=number
- Opérations: Coder l’ensembles des fonctions permettant de réaliser une opération sur les données.
def show(cityInfo): msg= "La ville de"+ cityInfo.name+" comporte " msg=msg+ cityInfo.population+ " habitants" print(msg)
- Tests: Mettre en œuvre les tests unitaires permettant de s’assurer du bon fonctionnement du module.
#tests if __name__=="__main__": #verification par jeu de test, comparaison visuelle, etc... ci=create() show(ci)
Le type Ball
Voici un autre exemple pour le type Ball
:
#Ball.py class Ball: pass #constructeur def create(radius): b=Ball() b.position=(0.0,0.0) b.speed=(0.0,0.0) b.radius=radius b.state='normal' return b #accesseurs def get_position(ball): return ball.position def get_speed(ball): return ball.speed def get_radius(ball): return ball.radius def get_state(ball): return ball.state #mutateurs def set_position(ball, x, y): ball.position=(x,y) def set_speed(ball,vx,vy): ball.speed=(vx,vy) def set_radius(ball,radius): ball.radius=radius def set_state(ball, state): ball.state=state def move(ball, dt): # deplacement de la balle sur un pas de temps dt #... a implementer... def stop(ball): ball.speed=(0,0) #tests if __name__=="__main__": #verification par comparaison visuelle my_ball=create(2) setSpeed(my_ball,2,4) print get_position(my_ball) move(my_ball, 10) print get_position(my_ball)
Module: En dehors du programme principal Main.py
(et sauf cas très particuliers), chacun des modules que vous concevrez devra correspondre à un type abstrait de données. Le module portera le nom du type. On fait toujours commencer le nom d’un type par une lettre majuscule, cela permet de différencier les variables des types. Le nom du module commence donc par une majuscule.
Constructeur: Au début d’un module on trouve le (ou les) constructeur(s) de la donnée. Un constructeur est une fonction qui capable de fabriquer la donnée.
#AbstractType.py class AbstractType: pass #constructeur def create(parametres): #creation data=AbstractType() #initialisation en fonction des paramètres ... #retour de la donnee return data
A l’extérieur du module, on pourra appeler le constructeur de la manière suivante :
import AbstractType d= AbstractType.create(...)
On s’obligera à toujours utiliser un constructeur et ne jamais créer soit même la donnée.
Accesseurs & Mutateurs: Ce sont des fonctions très simples qui permettent d’accéder au contenu des données. A l’occasion de ce cours, elle doivent être écrites de manière systématique pour chaque information définie par le type. Ces fonctions fournissent un outil de débogage très important, dès lors qu’on se force à les utiliser systématiquement pour manipuler les données.
#AbstractType.py #accesseurs/mutateurs def get_x(data): #recuperation de x dans data x=... return x
A l’extérieur du module, on appellera l’accesseur et le mutateur :
import AbstractType d= AbstractType.create(...) #reinitialisation de la valeur avec le mutateur AbstractType.setX(d,'12') #recuperation de la valeur avec accesseur x=AbstractType.getX(d)
4. Opérations: On regroupe dans le module toutes les fonctions qui manipulent le type de données correspondant. Le premier paramètre d’une opération est en général une donnée du type correspondant. La difficulté est de penser les fonctions pour qu’elles ne manipulent qu’un type à la fois. Il est souvent utile de consulter le graphe de dépendance des modules pour concevoir correctement les fonctions. Parfois il faut décomposer une fonction à travers les modules pour obtenir un résultat pertinent. Ce point sera abordé dans la partie conception.
#AbstractType.py #operations def operation(data): #action a realiser sur la donnee data... #renvoie eventuel d une valeur return ...
A l’extérieur du module, on appellera la fonction :
import AbstractType d= AbstractType.create(...) #realisation d une operation: AbstractType.operation(d)
Respect de la barrière d’abstraction: Respecter la barrière d’abstraction, c’est s’obliger à toujours manipuler une donnée avec des opérations fournies par le module correspondant. En dehors du code du module, il ne faut jamais modifier ou lire directement une donnée :
import Ball b= Ball.create(5) #ce qu il ne faut pas ecrire print('radius=', b.radius) #ce qu il faut ecrire print 'radius=', Ball.getRadius(b)
Même si cela vous paraît fastidieux, il vous est fortement conseillé de respecter la barrière d’abstraction dès le début du codage. Cette règle est un garde-fou qui empêche de commettre des erreurs de conception. L’expérience montre qu’à chaque fois qu’un projet n’a pas respecté la règle, il n’a pu aboutir que dans la “douleur”. Si on se rappelle qu’un développeur passe la majeur partie de son temps à déboguer, le temps perdu à mieux coder n’est pas perdu...
Soit le type suivant
Type : Player = struct position : int name : str items : list d'Item
Une opération move
incrémente la position du joueur de 1
Une opération add_item
ajout un Item
dans la liste d’items.
Le type Item
est implémenté par module Item.py
#Player.py
class Player: pass
def create(name="noname",position=0):
p=Player()
p.name=name
p.position=position
p.items=[]
return p
def get_name(p):
return p.name
def get_position(p):
return p.position
def get_items(p):
return p.items
#on peut aussi ajouter cet accesseur
def get_item(p,i):
if i >0 and i<len(p['items']):
return p.items[i]
else return None
def set_name(p,name):
p.name=name
def set_position(p,position):
p.position=position
def set_items(p,items):
p.items=items
def move(p):
p.position=p.position+1
def add_item(p,item):
p.items.append(item)
if __name__=='__main__':
'''ici les tests'''
Il est préférable de réaliser cet exercice en dehors de l’ENIBOOK.
#pasdemodule.py
#code moche sans module
cities=None
def init():
global cities
#graphe
info={'name':'Brest','nbSchool':7,'population':141000}
s1={'info':info,'roads':[]}
info={'name':'Chateaulin','nbSchool':1,'population':5000}
s2={'info':info,'roads':[]}
info={'name':'Quimper','nbSchool':5,'population':90000}
s3={'info':info,'roads':[]}
info={'name':'Landivisiau','nbSchool':1,'population':10000}
s4={'info':info,'roads':[]}
info={'name':'Landerneau','nbSchool':0,'population':15000}
s5={'info':info,'roads':[]}
cities=[s1,s2,s3,s4,s5]
addRoad(s1,s2,46)
addRoad(s1,s5,26)
addRoad(s2,s3,28)
addRoad(s2,s5,36)
addRoad(s4,s5,17)
def addRoad(v1,v2,km):
v1['roads'].append((v2,km))
v2['roads'].append((v1,km))
def getDistance(v1,v2):
#return 0 if no way
distance =0
for link in v1['roads']:
v,km=link
if v==v2:
break
return distance
def askCity():
global cities
name=raw_input('Choisissez un nom de ville')
for v in cities:
if v['info']['name']==name:
showInfo(v['info'])
return
print 'Pas de ville de ce nom'
def showInfo(i):
msg='La ville de '+i['name']+' a une popuation de '+str(i['population'])+' habitants.'
print msg
def main():
init()
askCity()
if __name__ == '__main__':
main()
#main.py
import Cities
cities=None
#ne pas confondre cities et Cities, respectivement la reference vers la donnee et le module qui decrit le type abstrait.
def init():
global cities
#graphe
my_cities=Cities.create()
Cities.add_city(cities,'Brest',7,141000)
Cities.add_city(cities,'Chateaulin',1,5000)
Cities.add_city(cities,'Quimper',5,90000)
Cities.add_city(cities,'Landivisiau',1,10000)
Cities.add_city(cities,'Landerneau',0,15000)
Cities.add_road(cities,'Brest','Chateaulin',46)
Cities.add_road(cities,'Brest','Landerneau',26)
Cities.add_road(cities,'Chateaulin','Quimper',28)
Cities.add_road(cities,'Chateaulin','Landerneau',36)
Cities.add_road(cities,'Landivisiau','Landerneau',17)
def ask_city():
global cities
name=raw_input('Choisissez un nom de ville')
Cities.show_city_info(cities,name)
def main():
init()
ask_city()
if __name__ == '__main__':
main()
#cityInfo
class CityInfo: pass
def create(nom,nb_school,population):
info=CityInfo()
info.name=nom
info.nb_school=nb_school
info.population=population
return info
def get_name(i):
return i['name']
def set_name(i,name):
i['name']=name
return
def get_population(i):
return i['population']
def set_population(i,population):
i['population']=population
return
def get_nb_school(i):
return i['nb_school']
def set_nb_school(i,nb):
i['nb_school']=nb
return
def show(i):
msg='La ville de '+i['name']+' a une popuation de '+str(i['population'])+' habitants.'
print msg
return
if __name__=='__main__':
'''ajouter les tests unitaires ici'''
#cities.py
#on importe le module que Cities utilisera
import CityInfo
def create():
#ici, on decide de code le type abstarait au moyen d'un liste... pourquoi pas!
return []
def add_city(cities,nom,nb_school,population):
#on appelle le construction du module CityInfo
info=CityInfo.create(nom,nb_school,population)
c={'info':info,'roads':[]}
cities.append(c)
def add_road(cities,name_1,name_2,km):
#trouver les sommet
s_1=find_city(cities,name_1)
s_2=find_city(cities,name_2)
#ajouter liens
s_1['roads'].append((s_2,km))
s_2['roads'].append((s_1,km))
def get_distance(name_1,name_2):
#return 0 if no way
distance =0
v_1=find_city(cities,name_1)
v_2=find_city(cities,name_2)
for link in v_1['roads']:
v,km=link
if v==v_2:
distance=km
break
return distance
def find_city(cities,name):
city=None
for sommet in cities:
#pour acceder au contenu de city en respectant la barrière d'abtraction, on utilise un accesseur
if CityInfo.get_name(sommet['info'])==name:
city=sommet
return city
def show_city_info(cities,name):
for v in cities:
if CityInfo.get_name(v['info'])==name:
CityInfo.show(v['info'])
return
print 'Pas de ville de ce nom'
if __name__=='__main__':
'''ajouter les tests unitaires ici'''