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

L’utilisation d’une fonction se décompose en deux étapes principales :

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

  2. 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 :

  1. définition de la fonction ;

  2. 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’instruction return la valeur de retour est ici None, 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 :

  1. définition : lignes 2 et 3 ;

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

Structuration conseillée

Pour structurer encore plus efficacement le programme, on utilisera l’organisation suivante

  1. Imports et définition des variables globales ;

  2. Définition des fonctions secondaires ;

  3. Définition de la fonction principale main() qui appelle les fonctions secondaires ;

  4. Appel protégé de la fonction principale main().

Voici un exemple de module respectant cette organisation.

 1# mymodule.py
 2
 3# 1. imports et définition des variables globales
 4
 5import math
 6
 7# 2. définition des fonctions secondaires
 8
 9def circle_area(diameter):
10    return math.pi * diameter**2 / 4
11
12# 3. définition de la fonction principale
13
14def main():
15    print(circle_area(10))
16
17# 4. appel protégé de la fonction principale
18
19if __name__ == '__main__':
20    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:

  1. importation du module complet et identification de la fonction lors de l’appel ;

  2. 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 objet

  • L’instruction return est optionnelle

  • Si 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 terminal

  • Il 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 -