Identification du problème

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).

Portée des variables

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 calculCarre1 utilise deux variables: nombre (le paramètre) et carre_du_nombre.
  • La sous-programme calculCarre2 utilise deux variables: a (le paramètre), indépendante de la variable a du programme principal et carre (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

Définition et utilisation

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.

Spécification et qualité

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.

Précisions importantes

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é…

Fonctions et méthodes connues

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 : renvoie True si la chaîne ne contient que des chiffres (False sinon) ; exemple d’utilisation : if chaine.isdigit() ;
  • def find(chaine: str, motif: str) -> int est 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() ; lower renvoie la chaîne en minuscules, et capitalize la 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 fonction sorted — trie (modifie) le tableau ; utilisation : tableau.sort().