Aller au contenu

Chp E. Les fonctions

1. Le principe

Dans un même programme, une opération, ou une séquence d'opérations peut intervenir à plusieurs reprises. Dans ce cas, il est intéressant de définir une fonction qui exécute ce bloc d'instructions. Découper un programme en fonctions élémentaires permet d'améliorer et de faciliter sa lisibilité. De même lorsqu'on résout un problème, il est souvent intéressant de le scinder en plusieurs petits problèmes (on pensera à l'adage diviser pour régner, ou divide and conquer en anglais !). Là encore la notion de fonction prend tout son sens. Nous allons voir dans ce chapitre comment cela s'utilise.

Les fonctions représentent une brique fondamentale de la programmation. Il est important de bien les maîtriser.

Un premier exemple inspiré des mathématiques

On va ici définir une fonction (comme en mathématiques) dont le rôle est de renvoyer le carré du nombre reçu :

def f(x):
    return x*x

print(f"L'image par f de {2} est {f(2)}")

Ce programme affichera L'image par f de 2 est 2

2. Implémentation en python

On peut imager la notion de fonction en python comme une extension du langage... c'est à dire que l'on enrichit le vocabulaire du langage de base par de nouveaux mots. Une fonction en informatique est assez proche de la notion de fonction mathématique : elle reçoit des arguments (éventuellement aucun), elle les traite et elle renvoie une valeur (éventuellement aucune). Ce sont les « éventuellement» qui font toute la différence avec les fonctions mathématiques !

À retenir : déclaration/utilisation des fonctions

On retiendra que l'utilisation des fonctions personnelles en python s'effectue en deux temps.

  1. D'abord on commence par définir la fonction. C'est à dire on explique ce qu'elle doit faire. Pour cela on utilise les mots clés def et  return. À ce stade, le code contenu dans la fonction n'a pas encore été exécuté.

  2. Pour utiliser, ou appeler la fonction on écrit son nom, suivi de parenthèses contenant éventuellement des paramètres.

La syntaxe en python pour définir une fonction est la suivante :

def nom_de_la_fonction(liste de paramètres) :
    """ la documentation... (docstring)"""
    Les instructions...
    return valeur de retour

Voyons sur un exemple détaillé les différents points à retenir pour définir une fonction :

# 1. déclaration
def fonction_carree(x : float) -> float :
    """ cette fonction reçoit un flottant et renvoie
    le carré de ce flottant 
    Ex : fonction_carree(2) renvoie 4
    """
    calcul = x*x
    return calcul

# 2. utilisation
print(fonction_carree(2))

On notera :

  • présence du mot clé def
  • ensuite le nom de la fonction (simple mais évocateur, sans accents, sans espaces)
  • ensuite des parenthèses contenant en principe des paramètres (mais les parenthèses peuvent être vides s'il n'y a pas de paramètres)
  • le ou les paramètres, séparés par des virgules. On notera dans l'exemple que l'on a fait suivre le paramètre du symbole ':' et de son type, ceci est facultatif mais c'est une bonne habitude (facilite la lecture entre autre...)
  • le type de la valeur de retour précédé des symboles -> (ceci est aussi facultatif, mais c'est une bonne habitude...)
  • le symbole ":" (impératif, à ne pas oublier)
  • Le contenu de la fonction est indenté. Cela crée un bloc pour identifier les instructions qui font partie de la fonction.
  • On commence par un texte descriptif aussi détaillé que possible entouré par des triples guillemets (on parle de la docstring)
  • Ensuite on place les instructions à effectuer
  • On termine avec le mot clé return suivi des valeurs de retour.

Application : découpage d'un problème en sous problèmes

Faire le TD E1 : on verra comment découper un problème en petits sous-problèmes à l'aide de la notion de fonction. Le problème abordé est la persistance multiplicative des nombres.

3. Variables locales, variables globales

Important !

La notion de variables locales et globales est central et très important à comprendre.

Le principe de base est que toute variable créée à l'intérieur d'une fonction est locale, c'est à dire n'est pas visible depuis l'extérieur de la fonction et heureusement !...

Illustration des variables locales vs globales

def g(): # on définit une fonction g 
    b = 2 # on définit une variable locale b à l'intérieur
    b = b + 10
    print(f"à l'intérieur de g : a={a} et b={b}")

a, b = 1, 1 # on définit a et b à l'extérieur
g() # on appelle la fonction g
print("à l'extérieur de g : a={a} et b={b}")

L'exécution de ce code va afficher :

à l'intérieur de g : a = 1 et b = 12
à l'extérieur de g : a = 1 et b = 1

On constate :

  1. que la variable b à l'intérieur de la fonction g est différente de la variable bà l'extérieur.

  2. que la variable a définie à l'extérieur de la fonction g est tout de même visible depuis l'intérieur de la fonction g mais en lecture uniquement (on ne peut pas la modifier). Si on tente de la modifier, une erreur est levée (UnboundLocalError), car la variable serait locale et globale à la fois, ce qui est problématique (voir ci-dessous une petite subtilité)

Tester dans la fenêtre ci-dessous :

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Une petite subtilité : une variable ne peut pas être globale et locale à la fois dans une fonction. Lorsque Python détecte dans une fonction une variable à la fois locale et globale, il lève une erreur de type UnboundLocalError. Heureusement, car sinon un code deviendrait vite illisible.

Par exemple le code suivant ne fonctionne pas:

def f(): # on définit une fonction f
    a = a + 1 # ici a est locale (à gauche du =) et globale (à dte) !!

a = 2 # a est une variable globale
f() # on appelle la fonction f

L'exécution va donner :

Traceback (most recent call last):
  File "<input>", line 5, in <module>
  File "<input>", line 2, in f
UnboundLocalError: cannot access local variable 'a' where it is
 not associated with a value

Question : comment faire si l'on veut effectivement modifier une variable globale a à l'aide d'une fonction ?

Réponse 1 (bonne pratique)

La bonne méthode est d'utiliser les paramètres et la valeur de retour :

def incrementer(n : int) -> int :
    """ cette fonction reçoit une nombre entier
    et renvoie cet entier augmenté de 1
    """
    return n+1

a = 2
a = incrementer(a)
# la variable a contient maintenant la valeur 3

Noter que le nom de la variable reçue par la fonction n n'a aucunement besoin d'être le même que celui de la variable globale a et heureusement !

Réponse 2 (mauvaise pratique)

Parfois, lorsque l'on veut modifier une variable globale, en allant vite pour faire quelques tests, on peut utiliser le mot clé global pour indiquer que l'on veut modifier la variable globale. C'est une mauvaise pratique, car cela nuit à la lisibilité du code et rend le code difficile à comprendre. Exemple :

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

3. Passage des arguments

Attention

Il y a un point important qu'il faut prendre en considération dans python : le passage des arguments aux fonctions se fait par référence, c'est à dire que l'objet passé en argument est partagé avec la fonction (on ne passe que son adresse mémoire) et qu'il risque d'être modifié si on n'en fait pas une copie.

Voyons un exemple pour illustrer cela :

def rallonge(lst : list) -> list :
    """ cette fonction reçoit une liste et y ajoute un élément.
    Attention la liste initiale est modifiée"""
    lst.append("valeur ajoutée")
    # absence de return, la fonction ne renvoie rien...

my_list = ['a','b', 'c']
print(my_list)
rallonge(my_list)
print(my_list)
# sortie :
#['a', 'b', 'c']
#['a', 'b', 'c', 'valeur ajoutée']

On constate qu'après l'appel de la fonction rallonge la liste my_list définie de façon globale a été modifiée dans la fonction ! En effet les mots lst et my_list désignent le même objet (la liste ['a', 'b', 'c']) puisque les arguments sont passés par référence.

Remarque 1 : on peut vérifier que les variables lst et my_list pointent sur le même objet en utilisant la fonction id() qui renvoie l'adresse mémoire de l'objet :

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

Remarque 2 : on peut aussi visualiser cela en utilisant un site très pratique pour analyser et comprendre le comportement de bouts de code : Python Tutor. Image de Python Tutor

Bonne pratique

Il est déconseillé d'utiliser cette fonctionnalité du passage par référence pour modifier un objet à l'intérieur d'une fonction (sauf si on a de bonnes raisons pour le faire 😉 ), car cela nuit fortement à la lisibilité du programme. En pratique on préférera faire une copie de la liste en début de fonction, puis renvoyer avec le mot return la nouvelle liste modifiée. On verra cela plus en détails dans le TD E2.

Par exemple, pour faire une copie d'une liste lst, on peut utiliser la syntaxe lst_copie = lst.copy().

Une petite subtilité (encore !) : le comportement décrit précédemment n'opère que si l'argument est dit mutable (comme une liste ou un dictionnaire). Sinon, quand l'argument est dit immuable (c'est le cas des nombres et des chaînes de caractères) une copie est automatiquement créée par la fonction, et par conséquent la variable utilisée dans la fonction est une copie de la variable globale et celle-ci ne sera pas modifiée. Voir encore le TD E2 pour des détails.

Conclusion sur les bonnes pratiques

Une fois que l'on a compris la différence entre une variable globale et une variable locale, pour ne pas se perdre dans toutes les subtilités évoquées, on suivra les bonnes pratiques suivantes :

  • Si l'on veut consulter (en lecture) une variable globale depuis l'intérieur d'une fonction, on peut le faire sans problème, il n'y a rien à faire de spécial.
  • Si l'on veut modifier une variable globale depuis l'intérieur d'une fonction, on utilisera les paramètres et la valeur de retour de la fonction (penser à l'exemple de l'incrémentation d'un entier vu plus haut). Dans le cas où l'objet est mutable (par exemple une liste), on pensera à faire une copie de l'objet (par exemple avec objet.copy()), pour ne pas modifier par inadvertance l'objet global.

4. Les TD E

Le TD E1 : introduction aux fonctions

Dans ce TD, on s'intéresse surtout à l'intérêt d'utiliser des fonctions pour découper un problème en sous-problèmes. Cela facilite la lecture du code. On n'entrera pas dans des subtilités de la notion de fonction, mais on verra comment définir une fonction et l'utiliser.

TD E1 version interactive capytale

Versions html : Énoncé et Correction

Le TD E2 : fonctions et passages des arguments

TD E2 version interactive capytale

Versions html : Énoncé et Correction (à venir)

Le TD E3 : fonctions, exercices

TD E3 version interactive capytale

Versions html : Énoncé et Correction (à venir)