10 Fonctions¶
10.1 Principe et généralités¶
En programmation, les fonctions sont très utiles pour réaliser plusieurs fois la même opération au sein d'un programme. Elles rendent également le code plus lisible et plus clair en le fractionnant en blocs logiques.
Vous connaissez déjà certaines fonctions Python. Par exemple math.cos(angle)
du module math
renvoie le cosinus de la variable angle
exprimé en radian. Vous connaissez aussi des fonctions internes à Python comme range()
ou len()
. Pour l'instant, une fonction est à vos yeux une sorte de « boîte noire » :
- À laquelle vous passez aucune, une ou plusieurs variable(s) entre parenthèses. Ces variables sont appelées arguments. Il peut s'agir de n'importe quel type d'objet Python.
- Qui effectue une action.
- Qui renvoie un objet Python ou rien du tout.
Tout cela est illustré schématiquement dans la figure ci-dessous.
Par exemple, si vous appelez la fonction len()
de la manière suivante :
voici ce qu'il se passe :
- vous appelez
len()
en lui passant une liste en argument (ici la liste[0, 1, 2]
) ; - la fonction calcule la longueur de cette liste ;
- elle vous renvoie un entier égal à cette longueur.
Autre exemple, si vous appelez la méthode ma_liste.append()
(n'oubliez pas, une méthode est une fonction qui agit sur l'objet auquel elle est attachée par un point) :
- Vous passez l'entier
5
en argument ; - la méthode
append()
ajoute l'entier5
à l'objetma_liste
; - et elle ne renvoie rien.
Aux yeux du programmeur, au contraire, une fonction est une portion de code effectuant une suite d'instructions bien particulière. Mais avant de vous présenter la syntaxe et la manière de construire une fonction, revenons une dernière fois sur cette notion de « boîte noire » :
-
Une fonction effectue une tâche. Pour cela, elle reçoit éventuellement des arguments et renvoie éventuellement quelque chose. L'algorithme utilisé au sein de la fonction n'intéresse pas directement l'utilisateur. Par exemple, il est inutile de savoir comment la fonction
math.cos()
calcule un cosinus. On a juste besoin de savoir qu'il faut lui passer en argument un angle en radian, et qu'elle renvoie le cosinus de cet angle. Ce qui se passe à l'intérieur de la fonction ne regarde que le programmeur. -
Chaque fonction effectue en général une tâche unique et précise. Si cela se complique, il est plus judicieux d'écrire plusieurs fonctions (qui peuvent éventuellement s'appeler les unes les autres). Cette modularité améliore la qualité générale et la lisibilité du code. Vous verrez qu'en Python, les fonctions présentent une grande flexibilité.
Pour finir sur les généralités, nous avons utilisé dans la Figure ci-dessus le terme programme principal (main en anglais), pour désigner l'endroit depuis lequel on appelle une fonction (on verra plus tard que l'on peut en fait appeler une fonction de n'importe où). Le programme principal désigne le code qui est exécuté lorsqu'on lance le script Python, c'est-à-dire toute la suite d'instructions en dehors des fonctions. En général, dans un script Python, on écrit d'abord les fonctions, puis le programme principal. Nous aurons l'occasion de revenir sur cette notion de programme principal plus tard dans ce chapitre, ainsi que dans le chapitre 13 Plus sur les fonctions.
10.2 Définition¶
Pour définir une fonction, Python utilise le mot-clé def
. Si on souhaite que la fonction renvoie quelque chose, il faut utiliser le mot-clé return
. Par exemple :
Notez que la syntaxe de def
utilise les deux-points comme les boucles for
et while
ainsi que les tests if
: un bloc d’instructions est donc attendu. De même que pour les boucles et les tests, l'indentation de ce bloc d'instructions (qu'on appelle le corps de la fonction) est obligatoire.
Dans l'exemple précédent, nous avons passé un argument à la fonction carre()
, qui nous a renvoyé (ou retourné) une valeur que nous avons immédiatement affichée à l'écran avec l'instruction print()
. Que veut dire valeur renvoyée ? Et bien cela signifie que cette dernière est récupérable dans une variable :
Ici, le résultat renvoyé par la fonction est stocké dans la variable res
.
Notez qu'une fonction ne prend pas forcément un argument et ne renvoie pas forcément une valeur, par exemple :
Dans ce cas, la fonction hello()
se contente d'afficher la chaîne de caractères "bonjour"
à l'écran. Elle ne prend aucun argument et ne renvoie rien. Par conséquent, cela n'a pas de sens de vouloir récupérer dans une variable le résultat renvoyé par une telle fonction. Si on essaie tout de même, Python affecte la valeur None
qui signifie rien en anglais:
Ceci n'est pas une faute car Python n'émet pas d'erreur, toutefois cela ne présente, la plupart du temps, guère d'intérêt.
10.3 Passage d'arguments¶
Le nombre d'arguments que l'on peut passer à une fonction est variable. Nous avons vu ci-dessus des fonctions auxquelles on passait zero ou un argument. Dans les chapitres précédents, vous avez rencontré des fonctions internes à Python qui prenaient au moins deux arguments. Souvenez-vous par exemple de range(1, 10)
ou encore range(1, 10, 2)
. Le nombre d'arguments est donc laissé libre à l'initiative du programmeur qui développe une nouvelle fonction.
Une particularité des fonctions en Python est que vous n'êtes pas obligé de préciser le type des arguments que vous lui passez, dès lors que les opérations que vous effectuez avec ces arguments sont valides. Python est en effet connu comme étant un langage au « typage dynamique », c'est-à-dire qu'il reconnaît pour vous le type des variables au moment de l'exécution. Par exemple :
>>> def fois(x, y):
... return x*y
...
>>> fois(2, 3)
6
>>> fois(3.1415, 5.23)
16.430045000000003
>>> fois("to", 2)
'toto'
>>> fois([1,3], 2)
[1, 3, 1, 3]
L'opérateur *
reconnaît plusieurs types (entiers, floats, chaînes de caractères, listes). Notre fonction fois()
est donc capable d'effectuer des tâches différentes ! Même si Python autorise cela, méfiez-vous tout de même de cette grande flexibilité qui pourrait conduire à des surprises dans vos futurs programmes. En général, il est plus judicieux que chaque argument ait un type précis (entiers, floats, chaînes de caractères, etc.) et pas l'un ou l'autre.
10.4 Renvoi de résultats¶
Un énorme avantage en Python est que les fonctions sont capables de renvoyer plusieurs objets à la fois, comme dans cette fraction de code :
En réalité Python ne renvoie qu'un seul objet, mais celui-ci peut être séquentiel, c'est-à-dire contenir lui-même d'autres objets. Dans notre exemple, Python renvoie un objet de type tuple
, type que nous avons vu dans le chapitre 8 Dictionnaires et tuples (souvenez-vous, il s'agit d'une sorte de liste avec des propriétés différentes). Notre fonction pourrait tout autant renvoyer une liste :
Renvoyer un tuple ou une liste de deux éléments (ou plus) est très pratique en conjonction avec l'affectation multiple, par exemple :
Cela permet de récupérer plusieurs valeurs renvoyées par une fonction et de les affecter à la volée à des variables différentes.
Une fonction peut aussi renvoyer un booléen :
def est_pair(x):
if x % 2 == 0:
return True
else:
return False
# Programme principal.
for chiffre in range(1, 5):
if est_pair(chiffre):
print(f"{chiffre} est pair")
Comme la fonction renvoie un booléen, on peut utiliser la notation if est_pair(chiffre):
qui équivaut à if est_pair(chiffre) == True:
. Il est courant d'appeler une fonction qui renvoie un booléen est_quelquechose()
car on comprend que ça pose la question si c'est vrai ou faux. En anglais, on trouvera la notation is_even()
. Nous reverrons ces notions dans le chapitre 13 Plus sur les fonctions.
10.5 Arguments positionnels et arguments par mot-clé¶
Jusqu'à maintenant, nous avons systématiquement passé le nombre d'arguments que la fonction attendait. Que se passe-t-il si une fonction attend deux arguments et que nous ne lui en passons qu'un seul ?
>>> def fois(x, y):
... return x*y
...
>>> fois(2, 3)
6
>>> fois(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fois() missing 1 required positional argument: 'y'
On constate que passer un seul argument à une fonction qui en attend deux conduit à une erreur.
Lorsqu'on définit une fonction def fct(x, y):
les arguments x
et y
sont appelés arguments positionnels (en anglais, positional arguments). Il est strictement obligatoire de les préciser lors de l'appel de la fonction. De plus, il est nécessaire de respecter le même ordre lors de l'appel que dans la définition de la fonction. Dans l'exemple ci-dessus, 2
correspondra à x
et 3
correspondra à y
. Finalement, tout dépendra de leur position, d'où leur qualification de positionnel.
Mais il est aussi possible de passer un ou plusieurs argument(s) de manière facultative et de leur attribuer une valeur par défaut :
Un argument défini avec une syntaxe def fct(arg=val):
est appelé argument par mot-clé (en anglais, keyword argument). Le passage d'un tel argument lors de l'appel de la fonction est facultatif. Ce type d'argument ne doit pas être confondu avec les arguments positionnels présentés ci-dessus, dont la syntaxe est def fct(arg):
.
Il est bien sûr possible de passer plusieurs arguments par mot-clé :
>>> def fct(x=0, y=0, z=0):
... return x, y, z
...
>>> fct()
(0, 0, 0)
>>> fct(10)
(10, 0, 0)
>>> fct(10, 8)
(10, 8, 0)
>>> fct(10, 8, 3)
(10, 8, 3)
On observe que pour l'instant, les arguments par mot-clé sont pris dans l'ordre dans lesquels on les passe lors de l'appel. Comment faire si l'on souhaitait préciser l'argument par mot-clé z
et garder les valeurs de x
et y
par défaut ? Simplement en précisant le nom de l'argument lors de l'appel :
Python permet même de rentrer les arguments par mot-clé dans un ordre arbitraire :
Que se passe-t-il lorsque nous avons un mélange d'arguments positionnels et par mot-clé ? Et bien les arguments positionnels doivent toujours être placés avant les arguments par mot-clé :
>>> def fct(a, b, x=0, y=0, z=0):
... return a, b, x, y, z
...
>>> fct(1, 1)
(1, 1, 0, 0, 0)
>>> fct(1, 1, z=5)
(1, 1, 0, 0, 5)
>>> fct(1, 1, z=5, y=32)
(1, 1, 0, 32, 5)
On peut toujours passer les arguments par mot-clé dans un ordre arbitraire à partir du moment où on précise leur nom. Par contre, si les deux arguments positionnels a
et b
ne sont pas passés à la fonction, Python renvoie une erreur.
>>> fct(z=0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fct() missing 2 required positional arguments: 'a' and 'b'
Préciser le nom des arguments par mot-clé lors de l'appel d'une fonction est une pratique que nous vous recommandons. Cela les distingue clairement des arguments positionnels.
L'utilisation d'arguments par mot-clé est habituelle en Python. Elle permet de modifier le comportement par défaut de nombreuses fonctions. Par exemple, si on souhaite que la fonction print()
n'affiche pas un retour à la ligne, on peut utiliser l'argument end
:
Nous verrons, dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne), que l'utilisation d'arguments par mot-clé est systématique lorsqu'on crée un objet graphique (une fenêtre, un bouton, etc.).
10.6 Variables locales et variables globales¶
Lorsqu'on manipule des fonctions, il est essentiel de bien comprendre comment se comportent les variables. Une variable est dite locale lorsqu'elle est créée dans une fonction. Elle n'existera et ne sera visible que lors de l'exécution de ladite fonction.
Une variable est dite globale lorsqu'elle est créée dans le programme principal. Elle sera visible partout dans le programme.
Ceci ne vous paraît pas clair ? Nous allons prendre un exemple simple qui vous aidera à mieux saisir ces concepts. Observez le code suivant :
# Définition d'une fonction carre().
def carre(x):
y = x**2
return y
# Programme principal.
var = 5
resultat = carre(var)
print(resultat)
Pour la suite des explications, nous allons utiliser l'excellent site Python Tutor qui permet de visualiser l'état des variables au fur et à mesure de l'exécution d'un code Python. Avant de poursuivre, nous vous conseillons de prendre 5 minutes pour tester ce site.
Regardons maintenant ce qui se passe dans le code ci-dessus, étape par étape :
- Étape 1 : Python est prêt à lire la première ligne de code.
- Étape 2 : Python met en mémoire la fonction
carre()
. Notez qu'il ne l'exécute pas ! La fonction est mise dans un espace de la mémoire nommé Global frame, il s'agit de l'espace du programme principal. Dans cet espace seront stockées toutes les variables globales créées dans le programme. Python est maintenant prêt à exécuter le programme principal.
- Étape 3 : Python lit et met en mémoire la variable
var
. Celle-ci étant créée dans le programme principal, il s'agira d'une variable globale. Ainsi, elle sera également stockée dans le Global frame.
- Étape 4 : La fonction
carre()
est appelée et on lui passe en argument l'entiervar
. La fonction s'exécute et un nouveau cadre est créé dans lequel Python Tutor va indiquer toutes les variables locales à la fonction. Notez bien que la variable passée en argument, qui s'appellex
dans la fonction, est créée en tant que variable locale. On remarquera aussi que les variables globales situées dans le Global frame sont toujours là.
- Étape 5 : Python est maintenant prêt à exécuter chaque ligne de code de la fonction.
- Étape 6 : La variable
y
est créée dans la fonction. Celle-ci est donc stockée en tant que variable locale à la fonction.
- Étape 7 : Python s'apprête à renvoyer la variable locale
y
au programme principal. Python Tutor nous indique le contenu de la valeur renvoyée.
- Étape 8 : Python quitte la fonction et la valeur renvoyée par celle-ci est affectée à la variable globale
resultat
. Notez bien que lorsque Python quitte la fonction, l'espace des variables alloué à la fonction est détruit. Ainsi, toutes les variables créées dans la fonction n'existent plus. On comprend pourquoi elles portent le nom de locales puisqu'elles n'existent que lorsque la fonction est exécutée.
- Étape 9 : Python affiche le contenu de la variable
resultat
et l'exécution est terminée.
Nous espérons que cet exemple guidé facilitera la compréhension des concepts de variables locales et globales. Cela viendra aussi avec la pratique. Nous irons un peu plus loin sur les fonctions dans le chapitre 13 Plus sur les fonctions. D'ici là, essayez de vous entraîner au maximum avec les fonctions. C'est un concept ardu, mais il est impératif de le maîtriser.
Enfin, comme vous avez pu le constater, Python Tutor nous a grandement aidé à comprendre ce qui se passait. N'hésitez pas à l'utiliser sur des exemples ponctuels, ce site vous aidera à visualiser ce qui se passe lorsqu'un code ne fait pas ce que vous attendez.
10.7 Principe DRY¶
L'acronyme DRY signifie Don't Repeat Yourself. Les fonctions permettent de satisfaire ce principe en évitant la duplication de code. En effet, plus un code est dupliqué plusieurs fois dans un programme, plus il sera source d'erreurs, notamment lorsqu'il faudra le faire évoluer.
Considérons par exemple le code suivant qui convertit plusieurs températures des degrés Fahrenheit en degrés Celsius :
>>> temp_in_fahrenheit = 60
>>> (temp_in_fahrenheit - 32) * (5/8)
17.5
>>> temp_in_fahrenheit = 80
>>> (temp_in_fahrenheit - 32) * (5/8)
30.0
>>> temp_in_fahrenheit = 100
>>> (temp_in_fahrenheit - 32) * (5/8)
42.5
Malheureusement, il y a une erreur dans la formule de conversion. En effet, la formule exacte est :
Il faut alors reprendre les lignes 2, 5 et 8 précédentes et les corriger. Cela n'est pas efficace, surtout si le même code est utilisé à différents endroits dans le programme.
En écrivant qu'une seule fois la formule de conversion dans une fonction, on applique le principe DRY :
>>> def convert_fahrenheit_to_celsius(temperature):
... return (temperature - 32) * (5/9)
...
>>> temp_in_fahrenheit = 60
>>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
15.555555555555557
>>> temp_in_fahrenheit = 80
>>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
26.666666666666668
>>> temp_in_fahrenheit = 100
>>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
37.77777777777778
Et s'il y a une erreur dans la formule, il suffira de ne la corriger qu'une seule fois, dans la fonction convert_fahrenheit_to_celsius()
.
10.8 Exercices¶
Pour le premier exercice, utilisez Python Tutor. Pour les exercices suivants, créez des scripts puis exécutez-les dans un shell.
10.8.1 Carré et factorielle¶
Reprenez l'exemple précédent à l'aide du site Python Tutor :
# Définition d'une fonction carre().
def carre(x):
y = x**2
return y
# Programme principal.
z = 5
resultat = carre(z)
print(resultat)
Analysez ensuite le code suivant et tentez de prédire sa sortie :
def calc_factorielle(n):
fact = 1
for i in range(2, n+1):
fact = fact * i
return fact
# Programme principal.
nb = 4
factorielle_nb = calc_factorielle(nb)
print(f"{nb}! = {factorielle_nb}")
nb2 = 10
print(f"{nb2}! = {calc_factorielle(nb2)}")
Testez ensuite cette portion de code avec Python Tutor, en cherchant à bien comprendre chaque étape. Avez-vous réussi à prédire la sortie correctement ?
Une remarque concernant l'utilisation des f-strings que nous avions abordées dans le chapitre 3 Affichage. On découvre ici une autre possibilité des f-strings dans l'instruction f"{nb2}! = {calc_factorielle(nb2)}"
: il est en effet possible d'appeler entre les accolades une fonction (ici {calc_factorielle(nb2)}
) ! Ainsi, il n'est pas nécessaire de créer une variable intermédiaire dans laquelle on stocke ce que retourne la fonction.
10.8.2 Puissance¶
Créez une fonction calc_puissance(x, y)
qui renvoie \(x^y\) en utilisant l'opérateur **
. Pour rappel :
Dans le programme principal, calculez et affichez à l'écran \(2^i\) avec \(i\) variant de 0 à 20 inclus. On souhaite que le résultat soit présenté avec le formatage suivant :
10.8.3 Pyramide¶
Reprenez l'exercice du chapitre 5 Boucles et comparaisons qui dessine une pyramide.
Dans un script pyra.py
, créez une fonction gen_pyramide()
à laquelle vous passez un nombre entier N
et qui renvoie une pyramide de \(N\) lignes sous forme de chaîne de caractères. Le programme principal demandera à l'utilisateur le nombre de lignes souhaitées (utilisez pour cela la fonction input()
) et affichera la pyramide à l'écran.
10.8.4 Nombres premiers¶
Reprenez l'exercice du chapitre 6 Tests sur les nombres premiers.
Créez une fonction est_premier()
qui prend comme argument un nombre entier positif n (supérieur à 2), et qui renvoie le booléen True
si n est premier et False
si n n'est pas premier. Déterminez tous les nombres premiers de 2 à 100. On souhaite avoir une sortie similaire à celle-ci :
10.8.5 Séquence complémentaire¶
Créez une fonction seq_comp()
qui prend comme argument une liste de bases et qui renvoie la séquence complémentaire d'une séquence d'ADN sous forme de liste.
Dans le programme principal, à partir de la séquence d'ADN
seq = ["A", "T", "C", "G", "A", "T", "C", "G", "A", "T", "C"]
affichez seq
et sa séquence complémentaire (en utilisant votre fonction seq_comp()
).
Rappel : la séquence complémentaire s'obtient en remplaçant A par T, T par A, C par G et G par C.
10.8.6 Distance 3D¶
Créez une fonction calc_distance_3D()
qui calcule la distance euclidienne en trois dimensions entre deux atomes. Testez votre fonction sur les 2 points A(0,0,0)
et B(1,1,1)
. Trouvez-vous bien \(\sqrt{3}\) ?
On rappelle que la distance euclidienne d entre deux points A et B de coordonnées cartésiennes respectives \((x_A, y_A, z_A)\) et \((x_B, y_B, z_B)\) se calcule comme suit :
10.8.7 Distribution et statistiques¶
Créez une fonction gen_distrib()
qui prend comme argument trois entiers : debut, fin et n. La fonction renverra une liste de \(n\) floats aléatoires entre debut et fin. Pour générer un nombre aléatoire dans un intervalle donné, utilisez la fonction uniform()
du module random, dont voici quelques exemples d'utilisation :
>>> import random
>>> random.uniform(1, 10)
8.199672607202174
>>> random.uniform(1, 10)
2.607528561528022
>>> random.uniform(1, 10)
9.000404025130946
Avec la fonction random.uniform()
, les bornes passées en argument sont incluses, c'est-à-dire qu'ici, le nombre aléatoire renvoyé est dans l'intervalle [1, 10].
Créez une autre fonction calc_stat()
qui prend en argument une liste de floats et qui renvoie une liste de trois éléments contenant respectivement le minimum, le maximum et la moyenne de la liste.
Dans le programme principal, générez 20 listes aléatoires de 100 floats compris entre 0 et 100 et affichez le minimum (min()
), le maximum (max()
) et la moyenne pour chacune d'entre elles. La moyenne pourra être calculée avec les fonctions sum()
et len()
.
Pour chacune des 20 listes, affichez les statistiques (valeur minimale, valeur maximale et moyenne) avec deux chiffres après la virgule :
Liste 1 : min = 0.17 ; max = 99.72 ; moyenne = 57.38
Liste 2 : min = 1.25 ; max = 99.99 ; moyenne = 47.41
[...]
Liste 19 : min = 1.05 ; max = 99.36 ; moyenne = 49.43
Liste 20 : min = 1.33 ; max = 97.63 ; moyenne = 46.53
Les écarts sur les statistiques entre les différentes listes sont-ils importants ? Relancez votre script avec des listes de 1000 éléments, puis 10 000 éléments. Les écarts changent-ils quand le nombre d'éléments par liste augmente ?
10.8.8 Distance à l'origine (exercice +++)¶
En reprenant votre fonction de calcul de distance euclidienne en trois dimensions calc_distance_3D()
, faites-en une version pour deux dimensions que vous appellerez calc_distance_2D()
.
Créez une autre fonction calc_dist2ori()
, à laquelle vous passez en argument deux listes de floats list_x
et list_y
représentant les coordonnées d'une fonction mathématique (par exemple \(x\) et \(sin(x)\)). Cette fonction renverra une liste de floats représentant la distance entre chaque point de la fonction et l'origine (de coordonnées \((0, 0)\)).
La figure 10 montre un exemple sur quelques points de la fonction \(sin(x)\) (courbe en trait épais). Chaque trait pointillé représente la distance que l'on cherche à calculer entre les points de la courbe et l'origine du repère de coordonnées \((0, 0\)).
Votre programme générera un fichier sin2ori.dat
qui contiendra deux colonnes : la première représente les \(x\), la seconde la distance entre chaque point de la fonction \(sin(x)\) à l'origine.
Enfin, pour visualiser votre résultat, ajoutez le code suivant tout à la fin de votre script :
# Création d'une image pour la visualisation du résultat.
import matplotlib.pyplot as plt
x = []
y = []
with open("sin2ori.dat", "r") as f_in:
for line in f_in:
coords = line.split()
x.append(float(coords[0]))
y.append(float(coords[1]))
fig, ax = plt.subplots(figsize=(6, 6))
ax.plot(x, y)
ax.set_xlabel("x")
ax.set_ylabel("Distance de sin(x) à l'origine")
fig.savefig("sin2ori.png")
Ouvrez l'image sin2ori.png
.
Le module matplotlib sera expliqué en détail dans le chapitre 21 Module matplotlib.