Complexité et duplication
Au-delà de 20-25 lignes ou de trois-quatre niveaux d’imbrication (if, for,
while), un code devient difficilement maintenable. De plus, certaines séquences
d’instructions peuvent être répétées en plusieurs endroits du programme. Exemple :
temperatures = []
saisie = input("Température (vide pour terminer) : ")
while saisie != "":
temperatures.append(float(saisie))
saisie = input("Température (vide pour terminer) : ")
#Calcul de la moyenne des températures :
total = 0
nb = len(temperatures)
for valeur in temperatures:
total = total + valeur
moyenne_temperatures = total / nb if nb > 0 else None
#Recherche de la température maximum :
val_max = temperatures[0] if len(temperatures) != 0 else None
for valeur in temperatures:
if valeur > val_max:
val_max = valeur
temperature_max = val_max
vents = []
saisie = input("Vitesse vent (km/h, vide pour terminer) : ")
while saisie != "":
vents.append(float(saisie))
saisie = input("Vitesse vent (km/h, vide pour terminer) : ")
#Calcul de la moyenne des vents :
total = 0
nb = len(vents)
for valeur in vents:
total = total + valeur
moyenne_vents = total / nb if nb > 0 else None
#Recherche du vent maximum :
val_max = vents[0] if len(vents) != 0 else None
for valeur in vents:
if valeur > val_max:
val_max = valeur
vent_max = val_max
print("Température moyenne = " + str(moyenne_temperatures) + "°C")
print("Température maximum = " + str(temperature_max) + "°C")
print("Vent moyen = " + str(moyenne_vents) + " km/h")
print("Vent maximum = " + str(vent_max) + " km/h")
Défauts de ce programme :
- les instructions de calcul de la moyenne et du recherche du maximum sont écrites deux fois ;
- le code est un peu trop long.
Réorganisation
Le code précédent peut avantageusement être réorganisé en utilisant des sous-programmes :
- pour la saisie,
- pour le calcul de la moyenne,
- pour la recherche du maximum.
from typing import List
def calculer_moyenne(valeurs: List[float]) -> float:
total = 0
for val in valeurs:
total = total + val
return total / len(valeurs) if len(valeurs) != 0 else None
def rechercher_maximum(valeurs: List[float]) -> float:
max_val = valeurs[0] if len(valeurs) != 0 else None
for val in valeurs:
if val > max_val:
max_val = val
return max_val
def saisir_valeurs(message: str) -> List[float]:
valeurs = []
saisie = input(message + " (vide pour terminer) : ")
while saisie != "":
valeurs.append(float(saisie))
saisie = input(message + " (vide pour terminer) : ")
return valeurs
#Programme principal :
if __name__ == "__main__":
temperatures = saisir_valeurs("Température")
vents = saisir_valeurs("Vitesse vent")
moyenne_temperatures = calculer_moyenne(temperatures)
temperature_max = rechercher_maximum(temperatures)
moyenne_vents = calculer_moyenne(vents)
vent_max = rechercher_maximum(vents)
print("Température moyenne = " + str(moyenne_temperatures) + "°C")
print("Température maximum = " + str(temperature_max) + "°C")
print("Vent moyen = " + str(moyenne_vents) + " km/h")
print("Vent maximum = " + str(vent_max) + " km/h")
Avec cette réorganisation :
- il n’y a plus de séquence d’instruction dupliquée ;
- chaque sous-programme est court et facile à maintenir.
Le programme principal (bloc if __name__ == "__main__":) utilise / appelle
les sous-programmes.
Les sous-programmes sont génériques : ils permettent la saisie et le calcul de la moyenne ou la recherche du maximum pour n’importe quelle série de nombres (ici des températures ou des vitesses de vent).
Localité des variables
La portée d’une variable est la zone du programme ou elle est accessible, typiquement dans le programme principal ou un sous-programme. On dit que les variables sont locales. Ainsi, une variable définie dans un sous-programme n’est pas accessible en dehors.
Exemple :
def sousProg1():
x = 0
x = x + a #erreur: 'a' n'est pas accessible ici
def sousProg2():
y = 1
y = y + x #erreur: 'x' n'est pas accessible ici
if __name__ == "__main__": #programme principal
a = 0
b = 1
sousProg1()
sousProg2()
z = y #erreur: 'y' n'est pas accessible ici
Cette fonctionnalité permet de réutiliser sans conflit les mêmes noms de variable dans plusieurs sous-programmes d’une même application.
Pour transmettre des informations entre sous-programmes, il faut utiliser des paramètres et renvoyer des valeurs.
Paramètres et valeur renvoyée
- Un sous-programme peut transmettre des données en paramètre à un autre sous-programme.
- Un sous-programme peut récupérer une donnée renvoyée par le sous-programme appelé.
Exemple :
def calculCarre1(nombre: int) -> int:
carre_du_nombre = nombre ** 2
return carre_du_nombre
def calculCarre2(a: int) -> int:
carre = a ** 2
return carre
if __name__ == "__main__":
a = int(input("Nombre : "))
carre = calculCarre1(a)
print("Carré : " + str(carre))
carre = calculCarre2(a)
print("Carré : " + str(carre))
Explications :
- Le programme principal utilise deux variables:
a,carre. - Le sous-programme
calculCarre1utilise deux variables:nombre(le paramètre) etcarre_du_nombre. - La sous-programme
calculCarre2utilise deux variables:a(le paramètre), indépendante de la variableadu programme principal etcarre(indépendante).
Lors de l’appel du sous-programme carre = calculCarre1(a), le code exécuté est :
nombre ← a #transmission appelant → sous-programme (paramètre)
carre_du_nombre = nombre ** 2
carre ← carre_du_nombre #cf: return carre_du_nombre
#transmission sous-programme -> appelant
Les instructions d’affectations ← sont invalides et données à titre indicatif.
De la même façon, lors de l’appel carre = calculCarre2(a) :
calculCarre1.a ← a #passage de paramètre / transmission de valeur
calculCarre1.carre = calculCarre1.a ** 2 #cf carre = a ** 2
carre ← calculCarre1.carre #renvoi de valeur; cf: return carre
Les variables du sous-programme sont ici préfixées par leur nom pour lever l’ambiguïté.
Variables globales (approfondissement)
C’est généralement une mauvaise pratique, mais à titre de référence, une variable définie dans le programme principal peut être rendu accessible aux sous-programmes. Sa portée est alors globale. Exemple
def calculCarre() -> int:
global nombre #instruction spécifique rendre une variable globale
nombre = nombre ** 2
return nombre
if __name__ == "__main__":
nombre = int(input("Nombre : "))
carre = calculCarre() #paramètre inutile (variable globale)
print("Carré : " + str(carre))
carre = calculCarre()
print("Carré : " + str(carre)) #nombre a été modifié dans la fonction
Le corps du sous-programme calculCarre introduit un effet de bord (modification
d’une variable qui impacte le sous-programme appelant (ici le programme principal).
Le code suivant n’aurait pas posé problème (mais reste une mauvaise pratique) :
def calculCarre() -> int:
global nombre #instruction spécifique rendre une variable globale
carre = nombre ** 2
return carre
if __name__ == "__main__":
nombre = int(input("Nombre : "))
print("Carré : " + str(calculCarre()))
print("Carré : " + str(calculCarre())) #ici, résultat identique
Il existe deux catégories de sous-programmes : les fonctions et les procédures, la différence étant qu’une procédure est une fonction qui ne renvoie pas de valeurs. C’est le terme fonction qui est communément utilisé pour évoquer les sous-programmes (y compris pour les procédures).
Définition d’une fonction
La définition d’une fonction en Python utilise le mot-clé def suivie du
nom de la fonction et de ses paramètres entre parenthèses :
def nom_fonction(param1: type, param2: type) -> type_retour:
corps de la fonction (instructions)
return valeur_renvoyee
*Le typage est obligatoire pour un développement de qualité, même si l’interpréteur Python n’effectue aucune vérification lors de l’utilisation de la fonction.*
Exemple :
def surface_rectangle(longueur: float, largeur: float) -> float:
surface = longueur * largeur
return surface
Utilisation d’une fonction
Lors de la définition d’une fonction, aucun code n’est exécuté ; c’est un peu comme écrire une recette. C’est lors de l’utilisation de la fonction que son code sera exécuté. Utiliser une fonction, c’est l’appeler, en lui transmettant les paramètres attendus et en récupérant éventuellement la valeur renvoyée.
Le développeur qui écrit la fonction définit des paramètres formels (noms
utilisés dans la fonction — cf ligne def …). Il ne connaît pas à l’avance
quels paramètres effectifs seront transmis lors de l’appel, ni leurs noms
dans le code appelant. Exemple :
longueur = 15.0
largeur = 10.0
surface_salle = surface_rectangle(longueur, largeur) #même noms
print("Surface salle : " + str(surface_salle) + " m²")
cote_A = float(input("Longueur du jardin : "))
cote_B = float(input("Largeur du jardin : "))
surface_jardin = surface_rectangle(cote_A, cote_B) #noms différents
#longueur ← cote_A
#largeur ← cote_B
print("Surface jardin : " + str(surface_jardin) + " m²")
surface_piscine = surface_rectangle(4, 3) #valeurs
#longueur ← 4
#largeur ← 3
print("Surface : " + str(surface_piscine) + " m²")
longueur / largeur, cote_A / cote_B et 4 / 5 sont trois exemples
de paramètres effectifs utilisés pour appeler la fonction.
Définition d’une fonction (bis)
Dans un soucis de qualité, la précédente syntaxe pour définir une fonction doit être enrichie pour apporter davantage d’informations aux (autres) développeurs :
- indiquer le rôle de la fonction (ce qui ne dispense pas de choisir un nom significatif) ;
- préciser le rôle des paramètres :
@param nom_parametre explications; - préciser ce qui est renvoyé par la fonction :
@return explications.
La syntaxe @param et @return est celle du standard “JavaDoc” (d’autres sont possibles).
Cette documentation permet :
- la génération d’un site présentant l’API) d’une bibliothèque de fonction ; exemple : API Java ;
- l’aide contextuelle ou l’auto-complétion des éditeurs de code (plus précise que celle offerte par les “Intelligences Artificielles” génératives).
Exemple :
def surface_rectangle(longueur: float, largeur: float) -> float:
"""
Calcule la surface d'un rectangle.
@param longueur la longueur du plus grand côté du rectangle
@param largeur la longueur du plus petit côté du rectangle
@return: la surface du rectangle
"""
return longueur * largeur
Spécification complète d’une fonction
La spécification complète d’une fonction inclut des pré et post conditions :
- les préconditions précisent ce que doit respecter l’utilisateur de la fonction ;
- les postconditions sont les garanties qu’apporte le développeur de la fonction.
Exemple 1 :
def surface_rectangle(longueur: float, largeur: float) -> float:
"""
Calcule la surface d'un rectangle.
@param longueur la longueur du plus grand côté du rectangle
@param largeur la longueur du plus petit côté du rectangle
@return: la surface du rectangle
préconditions: longueur > 0, largeur > 0
postcondition: surface_rectangle > 0
"""
return longueur * largeur
Exemple 2A (sans pré / post-conditions) :
from typing import List
def minimum(tableau: List[float]) -> float:
"""
Renvoie la plus petite valeur du tableau.
@param tableau le tableau de valeurs
@return la plus petite valeur du tableau ou None s'il est vide
"""
min_val = None
if len(tableau) > 0:
min_val = tableau[0]
for nombre in tableau[1:]:
if nombre < min_val:
min_val = nombre
return min_val
Exemple 2B (avec ajout d’une précondition) :
from typing import List
def minimum(tableau: List[float]) -> float:
"""
Renvoie la plus petite valeur du tableau.
@param tableau le tableau de valeurs
@return la plus petite valeur du tableau
précondition: le tableau est trié par ordre croissant et n'est pas vide
"""
return tableau[0]
Cet exemple démontre l’impact que peuvent avoir des préconditions et postconditions sur un algorithme.
Point d’entrée du programme
Le point d’entrée d’un programme est l’emplacement où l’exécution du programme
commence. Dans de nombreux langages, il s’agit de la fonction main, mais
en python, une bonne pratique consiste à le placer dans un bloc :
if __name__ == "__main__":
#point d'entrée du programme
Confusion paramètre / saisie
Une erreur commune chez les débutants est de faire ressaisir dans la fonction les valeurs des paramètres, ce qui donne, cet exemple quelque peu absurde :
- appel : “Calcule la surface d’un rectangle de longueur 5 et de largeur 3”.
- fonction : “Quelle est la longueur ?”, “Quelle est la largeur ?”
Il ne faut donc pas écrire :
def surface_rectangle(longueur: float, largeur: float) -> float:
longueur = float(input("Longueur : ")) #ne pas faire ça : longueur et
largeur = float(input("Largeur : ")) #largeur sont transmis en
surface = longueur * largeur #paramètre à la fonction !
return surface
if __name__ == "__main__":
resultat = surface_rectangle(3, 2)
print("Surface : " + str(resultat))
Mais il faut écrire :
def surface_rectangle(longueur: float, largeur: float) -> float:
surface = longueur * largeur
return surface
if __name__ == "__main__":
longueur = float(input("Longueur : "))
largeur = float(input("Largeur : "))
surface_rectangle(longueur, largeur)
Une bonne pratique consiste à séparer les fonctions qui font de l’affichage
ou de la saisie de celles qui font des calculs. Cela facilite la réutilisation
des fonctions de calcul dans des programmes avec interface graphique qui n’utilisent
pas les instructions print et input.
Confusion valeur renvoyée / affichage
Une autre erreur habituelle est de confondre afficher (print) et renvoyer
(return). C’est au sous-programme appelant de récupérer la valeur renvoyée
(souvent en l’affectant à une variable), afin d’en disposer ensuite à sa convenance.
Il ne faut donc pas écrire :
def surface_rectangle(longueur: float, largeur: float) -> float:
#rôle de la fonction : calculer et renvoyer la surface d'un rectangle
surface = longueur * largeur
print("Sfc fcn : " + str(surface)) #print ≠ return
if __name__ == "__main__":
resultat = surface_rectangle(3, 2)
print("Sfc prg : " + str(resultat)) #la fonction n'a rien renvoyé…
Dans ces explications, T désigne un type générique. Il peut s’agir de nombres
(int ou float), de chaînes de caractères (str)…
Fonctions de la bibliothèque standard Python
def print (message: str): affiche le message ;def input (l'invite: str) -> str: affiche l’invite (question) et renvoie la saisie (réponse) de l’utilisateur ;def int (chaine: str) -> int: renvoie le nombre correspondant à la chaîne de caractères ;def str (nombre: int) -> str: renvoie la chaîne de caractères correspondant au nombre ;def len (chaine: str) -> int: renvoie le nombre de caractères de la chaîne ;def len (tableau: List) -> int: renvoie le nombre d’éléments du tableau ;def range (debut: int = 0, fin: int, pas: int = 1) -> List[int]: renvoie la liste des nombres entre début et fin (exclu) ;def randint (debut: int, fin: int) -> int: renvoie un nombre aléatoire entre début et fin (inclus) ;def round (nombre_flottant, nb_decimales): arrondi aux nombres de chiffres indiqués après la virgule.def chr(code_ascii: int) -> str: renvoie le caractère correspondant au code ASCII ;def ord(caractère: str) -> int: renvoie le code ASCII du caractère.def sorted(tableau: List[T]) -> List[T]: renvoie un nouveau tableau à partir des éléments du premier trié.
Une méthode est une fonction, mais écrite avec une syntaxe objet : le premier
paramètre des fonctions est “déplacé” devant : fcn(objet, param1, …) → objet.fcn(param1, ….
Méthodes qui s’appliquent aux chaînes
def isdigit(chaine: str) -> bool: renvoieTruesi la chaîne ne contient que des chiffres (Falsesinon) ; exemple d’utilisation :if chaine.isdigit();def find(chaine: str, motif: str) -> intest une méthode qui renvoie l’indice du motif dans la chaîne (ou -1 si non trouvé) ; utilisation : `indice = chaine.find(motif) ;def replace(chaine: str, motif: str, remplacement: str) -> str: renvoie une nouvelle chaîne ou toutes les occurences de motif de la chaîne sont remplacées ; utilisation :nouvelle = chaine.replace(motif, par);def upper(chaine: str) -> str: renvoie la chaîne en majuscules ; utilisation :chaine_majuscules = chaine.upper();lowerrenvoie la chaîne en minuscules, etcapitalizela chaîne avec seulement la première lettre en majuscules ;def split(chaine: str, separateur: str) -> List[str]: découpe une chaîne et renvoie le tableau de (sous-)chaînes ainsi obtenu ; utilisation:tableau = chaine.split(separateur);def join(separateur: str, tableau: List[str]) -> str: fusionne les chaînes d’un tableau et renvoie la chaîne ainsi obtenue ; utilisation:chaine = separateur.join(tableau).
Méthodes qui s’appliquent aux tableaux
def append(tableau: List[T], valeur: T): ajoute une valeur au tableau ; utilisation :tableau.append(valeur);def insert(tableau: List[T], indice: int, valeur: T): insère la valeur à l’indice spécifié dans le tableau ;def pop(tableau: List[T], indice: int = len(tableau) - 1) -> T: retire du tableau et renvoie la valeur à l’indice spécifié (le dernier élément par défaut) ; utilisation :dernier_element = tableau.pop();def sort(tableau: List[T])— à ne pas confondre avec la fonctionsorted— trie (modifie) le tableau ; utilisation :tableau.sort().