Fonctions et modules
Une vidéo de présentation des fonctions…
Les fonctions permettent de structurer le code, de le rendre plus lisible, d’en faciliter la maintenance, en évitant la redondance. L’utilisation de fonctions (et, on le verra plus tard, de classes) fait parties des bonnes pratiques de la programmation, et est donc très vivement encouragée.
Les fonctions
Définition
Une fonction se définit avec l’instruction def
suivie du nom de la fonction et de la liste de ses arguments. Comme pour les instructions if
, for
et while
le corps de la fonction est constitué d’un bloc d’instructions indenté. Prenons l’exemple de la suite de Fibonacci dont le code est défini dans un fichier fonctions.py
.
1# filename : fonctions.py
2def fib(n):
3 a, b = 0, 1
4 while a < n:
5 print(a, end=' ')
6 a, b = b, a+b
7 print()
Note
En Python, le passage des arguments ne se fait ni par copie (comme en C) ni par référence (comme en Java). Il se fait par affectation. L’article Pass-by-value, reference, and assignment fait le point sur le sujet ;
Il n’est pas nécessaire de préciser le type des paramètres ni le type de retour. Il est cependant possible d’associer ces informations à la définition de la fonction avec les annotations.
Exécution
Exécuter le code ci dessus à partir de la console ne produit rien:
$ python fonctions.py
$
C’est tout à fait normal, car le code ne contient pour le moment que la définition de l’objet fib
(en Python tout est objet) de type fonction, comme on peut le constater dans l’interpréteur python:
>>> import fonctions
>>> fonctions.fib
<function fib at 0x0187D6A8>
>>> type(fonctions.fib)
<class 'function'>
Structuration
Un programme se décompose en deux étapes principales :
la définition de la fonction, qui décrit la transformation des arguments (données d’entrée) en un objet retourné (donnée de sortie) ;
et son appel qui applique cette transformation à des paramètres concrets.
Ce qui précède relève de la première étape.
La deuxième étape est illustrée ci dessous, en appelant la fonction avec le paramètre 50
>>> fonctions.fib(50)
0 1 1 2 3 5 8 13 21 34
On peut également inclure l’appel de la fonction dans le programme.
1# filename : fonctions.py
2def fib(n):
3 a, b = 0, 1
4 while a < n:
5 print(a, end=' ')
6 a, b = b, a+b
7 print()
8
9fib(50)
Cette fois l’exécution du script produit le résultat attendu dans le terminal:
$ python fonctions.py
0 1 1 2 3 5 8 13 21 34
Important
Par la suite, les programmes seront tous structurés de façon identique et devront comporter 2 parties clairement identifiées :
définition de la fonction ;
appel.
Valeur de retour
La valeur de retour est indiquée par l’instruction (optionnelle) return
. Si celle ci n’est pas présente, la valeur par défaut None
est retournée:
1# filename : fonctions-2.py
2def fib(n):
3 a, b = 0, 1
4 while a < n:
5 print(a, end=' ')
6 a, b = b, a+b
7 print()
8
9print(fib(50))
A la ligne 9, séquentiellement, on a:
un appel à
fib(50)
qui produit l’affichage de la suite ;puis la valeur de retour de la fonction
fib()
est passée àprint()
. Puisque la fonction ne possède pas d’instructionreturn
la valeur de retour est iciNone
, qui est affichée dans le terminal.
L’exécution du code produit le résultat attendu:
$ python fonctions2.py
0 1 1 2 3 5 8 13 21 34
None
Contrairement à l’exemple ci dessus, c’est une bonne pratique que de spécifier explicitement une valeur de retour pour les fonctions. Ici on préférera donc écrire :
1# filename : fonctions-2.py
2def fib(n):
3 a, b = 0, 1
4 while a < n:
5 print(a, end=' ')
6 a, b = b, a+b
7 print()
8 return None
9
10print(fib(50))
Dans la grande majorité des cas, la valeur de retour existe, comme c’est le cas ci dessous.
1# filename : square.py
2def square(x):
3 return x**2
4
5print(square(7))
On retrouve la structuration évoquée ci dessus :
définition : lignes 2 et 3 ;
appel : ligne 5.
Lorsqu’on exécute le code:
$ python square.py
49
Ici la fonction square()
ne produit aucun affichage par elle même. Son appel retourne une valeur qui est passée à la fonction print()
qui se charge de l’affichage. Ce sera une bonne pratique de confier :
à la fonction : la construction d’un objet ;
au code appelant : l’affichage éventuel.
Portée des variables
Les variables définies à l’intérieur de la fonction ont une portée locale. On considère le programme suivant, en observant la structuration :
1# 04-portee.py
2
3def f():
4 print("entering f()...")
5 x = 'black'
6 print("in f(), x =", x)
7 print('exiting f()')
8 return None
9
10x = 'white'
11print("before f(), x =", x)
12f()
13print("after f(), x =", x)
Exercice…
Identifier la portion de code concerné par :
la définition de la fonction ;
son appel.
Identifier la séquence ordonnée des lignes exécutées lorsqu’on lance le programme avec python 04-portee.py.
On constate ici que la valeur de x
n’est pas modifiée par l’appel de la fonction:
$ python 04-portee.py
before f(), x = white
entering f()...
in f(), x = black
exiting f()
after f(), x = white
Les variables définies à l’extérieur de la fonction sont accessibles à l’intérieur.
1def g():
2 print('entering g()')
3 print("in g(), x =", x)
4 print('exiting g()')
5 return None
6
7x = 'white'
8print("before g(), x =", x)
9g()
10print("after g(), x =", x)
L’affichage montre bien que la variable x
est accessible à l’intérieur de la fonction, alors qu’elle est définie à l’extérieur:
before g(), x = white
entering g()
in g(), x = white
exiting g()
after g(), x = white
Bien que ce ne soit pas recommandé, il est possible de modifier une variable définie à l’extérieur du corps de la fonction, en utilisant le mot clé global
:
1def h():
2 print('entering h()')
3 global x
4 print("in h(), before redefinition, x =", x)
5 x = 'blue'
6 print("in h(), after redefinition, x =", x)
7 print('exiting h()')
8
9x = 'white'
10print("before h(), x = ", x)
11h()
12print("after h(), x = ", x)
La variable x
définie à l’extérieur de la fonction est bien modifiée par celle ci:
before h(), x = white
entering h()
in h(), before redefinition, x = white
in h(), after redefinition, x = blue
exiting h()
after h(), x = blue
Les modules
Un fichier contenant des instructions Python s’appelle un module. Un module peut contenir une ou plusieurs fonctions.
Contenu et structure
Un module peut provenir :
du core language, c’est à dire être accessible sans opération particulière ;
de la bibliothèque standard Python et être accessible après une simple instruction
import
;d’une tierce partie et être accessible après installation du module externe et l’instruction
import
;
On développe ses propres modules en faisant appel aux modules pré existants décrits ci dessus.
A l’import, l’ensemble du code du module est exécuté, ce qui n’est pas toujours souhaitable lorsqu’un module comprend à la fois la définition des fonctions mais également leur utilisation. Ceci semble contradictoire avec la consigne donnée plus haut, de regrouper dans un seul et même fichier (module) la définition des fonctions et leur appel. Mais il n’en est rien car Python a prévu une construction spéciale pour « protéger » la partie du code qui correspond à l’appel des fonctions.
# mymodule.py
def f():
print("inside f()")
if __name__ == '__main__':
print("calling f()")
f()
Lorsque le module est importé (au travers de l’instruction import
) l’ensemble des fonctions est chargé en mémoire mais le code encapsulé dans la construction if
n’est pas exécuté:
import mymodule
ne produit aucun affichage mais la fonction f()
est utilisable dans la suite du script.
A contrario, si le module est exécuté:
$ python mymodule.py
Le code encapsulé dans la construction if
est exécuté:
calling f()
inside f()
C’est une possibilité très intéressante car elle permet l’utilisation du même module lors de la phase de conception et lors de son utilisation. Cette bonne pratique est très vivement encouragée.
Pour structurer encore plus efficacement le programme, on préférera grouper le code d’appel dans une fonction main()
. Par la suite cette structuration sera privilégiée :
1# mymodule.py
2def f():
3 print("inside f()")
4
5def main():
6 print("calling f()")
7 f()
8
9if __name__ == '__main__':
10 main()
Exercice…
Identifier la séquence ordonnée des lignes exécutées lorsqu’on lance le programme ci dessus.
Importation
Il existe deux manières d’importer et donc d’utiliser une fonction appartenant à un module. Chacune d’entre elles vise à identifier cette fonction de façon univoque:
importation du module complet et identification de la fonction lors de l’appel ;
importation d’une fonction identifiée du module.
Dans le premier cas, on importe le module entier, et le lien entre la fonction et le module est précisé lors de l’appel:
>>> import mymodule
>>> mymodule.f()
Dans le deuxième cas, le lien entre la fonction et le module est précisé à l’import, et il n’est pas nécessaire de le préciser lors de l’appel:
>>> from mymodule import f
>>> f()
Lorsqu’il n’y aura pas d’ambiguïté sur le nom de la fonction, on choisira de préférence la seconde un peu plus concise.
Les docstrings
Une bonne pratique est de documenter son code de façon générale, et les fonctions en particulier. Python met en oeuvre la notion de docstring
qui permet d’exploiter une chaîne de caractère définie sur la ligne suivant la définition de la fonction:
1# fact.py
2def fact(n):
3 """
4 Retourne la factorielle de n.
5
6 Args:
7 n: valeur entiere positive
8
9 Returns:
10 fact(n) : n*(n-1)* ... * 2
11 """
12 if n <= 0:
13 return 'n must be strictly positive'
14 if n <= 2:
15 return n
16 return n*fact(n-1)
On obtient ainsi une aide sur la fonction avec help()
:
>>> from fact import fact # from fact.py module import fact() function
>>> fact(5)
120
>>> help(fact)
Help on function fact in module fonctions:
fact(n)
Retourne la factorielle de n.
Args:
n: valeur entiere positive
Returns:
fact(n) : n*(n-1)* ... * 2
Avertissement
La docstring doit débuter sur la ligne immédiatement après la définition de la fonction.
Les doctests
On peut inclure dans les docstring
des sessions interactives qui seront exécutées lors de l’appel. Ces sessions interactives comprennent deux parties :
l’appel de la fonction ;
et le résultat attendu.
Les résultats obtenus lors de l’appel seront automatiquement comparés aux résultats attendus.
1# fact.py
2def fact(n):
3 """
4 Retourne la factorielle de n.
5
6 Args:
7 n: valeur entière >= 2
8
9 Returns:
10 fact(n) : n*(n-1)* ... * 2
11
12 >>> fact(-1)
13 'n must be strictly positive'
14 >>> fact(0)
15 'n must be strictly positive'
16 >>> fact(1)
17 1
18 >>> fact(2)
19 2
20 >>> fact(5)
21 120
22 >>> fact(10)
23 3628800
24 """
25 if n <= 0:
26 return 'n must be strictly positive'
27 if n <= 2:
28 return n
29 return n*fact(n-1)
Les tests sont lancés en faisant appel au module doctest
de la bibliothèque standard. Lorsque les tests passent, aucun affichage supplémentaire n’est produit:
$ python -m doctest fact.py
$
On peut cependant obtenir un affichage plus détaillé avec l’option -v
:
$ python -m doctest fact.py -v
Trying:
fact(-1)
Expecting:
'n must be strictly positive'
ok
Trying:
fact(0)
Expecting:
'n must be strictly positive'
ok
Trying:
fact(1)
Expecting:
1
ok
Trying:
fact(2)
Expecting:
2
ok
Trying:
fact(5)
Expecting:
120
ok
Trying:
fact(10)
Expecting:
3628800
ok
1 items had no tests:
fact
1 items passed all tests:
6 tests in fact.fact
6 tests in 2 items.
6 passed and 0 failed.
Test passed.
Si la fonction est mal conçue, un ou plusieurs tests peuvent échouer.
1# fact.py
2def fact(n):
3 """
4 Retourne la factorielle de n.
5
6 Args:
7 n: valeur entière >= 2
8
9 Returns:
10 fact(n) : n*(n-1)* ... * 2
11
12 >>> fact(-1)
13 'n must be strictly positive'
14 >>> fact(0)
15 'n must be strictly positive'
16 >>> fact(1)
17 1
18 >>> fact(2)
19 2
20 >>> fact(5)
21 120
22 >>> fact(10)
23 3628800
24 """
25 if n < 0:
26 return 'n must be strictly positive'
27 if n <= 2:
28 return n
29 return n*fact(n-1)
Ici, le test fact(0)
échoue:
> python -m doctest fact.py
**********************************************************************
File "fact.py", line 13, in fact.fact
Failed example:
fact(0)
Expected:
'n must be strictly positive'
Got:
0
**********************************************************************
1 items had failures:
1 of 6 in fact.fact
***Test Failed*** 1 failures.
On peut également intégrer l’exécution des tests dans le module lui même.
import doctest
if __name__ == '__main__':
doctest.testmod()
L’utilisation des doctest
est une bonne pratique et à ce titre, elle est vivement recommandée.
Ce qu’il faut retenir
L’instruction est utilisée pour la définition d’une fonction
L’instruction
return
permet de retourner un objetL’instruction
return
est optionnelleSi une fonction ne retourne rien, l’objet renvoyé est
Null
Les 2 phases (dans l’ordre logique) de l’utilisation d’une fonction sont la et l”
La définition et l’appel de la fonction peuvent se trouver dans le même fichier
La définition et l’appel de la fonction peuvent se trouver dans des fichiers différents
Les variables définies à l’intérieur de la fonction sont accessibles depuis le code d’appel
Les variables définies dans le code d’appel sont accessibles à l’intérieur de la fonction
Un module est un fichier Python
Le code encapsulé dans une construction
if __name__
est exécuté si le module est importéLe code encapsulé dans une construction
if __name__
est exécuté si le module est exécuté depuis le terminalIl existe façons d’importer une fonction appartenant à un module
Une docstring est utile pour documenter le code
Une docstring peut contenir des doctests
Un doctest permet de vérifier le comportement d’une fonction pour tous les cas de figure
Un doctest permet de vérifier le comportement d’une fonction pour un cas de figure
Un doctest comprend un contexte d’appel et le résultat attendu
Un doctest comprend lignes
Pour obtenir un affichage détaillé, on utilise l’option -