1. Fonctions#
Mis à jour : May 15, 2025, lecture : 18 minutes minimum, PhL.
Ce premier chapitre présente des rappels et quelques compléments sur les fonctions.
1.1. Rappels#
Une fonction est une suite d’instructions qui effectue un calcul ou réalise une action.
Une fonction est désignée par son nom, comme par exemple en mathématiques la fonction \(f\) ou les fonctions trigonométriques \(\cos, \sin\), …
Le calcul ou l’action réalisé par une fonction peut dépendre d’aucun, d’un ou de plusieurs paramètres. Par exemple en mathématiques, le calcul de \(f(x)\) dépend de la valeur du paramètre \(x\).
Définir. Une fonction doit d’abord être définie avant de pouvoir être utilisée.
la fonction \(f(x)\) doit être définie avant de pouvoir calculer \(f(3)\) par exemple.
Appeler. Appeler une fonction veut dire utiliser une fonction pour effectuer un calcul ou une action.
\(f(3)\) est l’appel de la fonction \(f(x)\) pour \(x = 3\).
1.1.1. Utiliser une fonction existante#
Les fonctions mathématiques comme les fonctions trigonométriques, logarithmes, exponentielles, puissances … sont “fournies” avec le langage de programmation. Elle sont définies et donc appelables.
Techniquement, elles sont regroupées sous la forme de bibliothèques de fonctions aussi appelées modules en python.
L’exemple suivant illustre l’appel de la fonction cos()
.
from math import cos, pi
# pour utiliser la fonction cos et la valeur pi définis dans le module math
c = cos(pi/3)
# calcul qui utilise cos et pi, puis affectation du résultat dans la variable c
print(c)
# affichage de la valeur de la variable c
0.5000000000000001
A retenir
Les parenthèses (...)
derrière le nom de la fonction : c’est à ça qu’on reconnaît une fonction !
A retenir
La construction
from module import fonction1, fonction2
permet d’utiliser les fonctions fonction1
et fonction2
définies dans la bibliothèque module
Retourner un résultat. Dans cet exemple, l’appel de la fonction
cos()
effectue un calcul et retourne un résultat : ici 0.5000000000000001.
Remarques.
Une fonction peut ne pas retourner de résultat.
Je ne connais pas nécessairement comment la fonction calcule son résultat.
En revanche, je connais le résultat attendu par l’appel de la fonction pour certains paramètres.
Exemple important à comprendre. Dans le code précédent :
print(c)
est l’appel à la fonctionprint()
cet appel réalise une action : afficher la valeur de
c
cet appel ne retourne aucun résultat (voir * ci-dessous), et surtout pas la valeur de
c
!
(*) Pour être exact, le print()
de python retourne la valeur None
.
res = print("toto")
print(res)
toto
None
Paramètre formel vs. paramètre effectif.
il peut y avoir 0, un ou plusieurs paramètres
un paramètre est appelé paramètre formel lorsqu’il apparait dans la définition de la fonction
un paramètre est appelé paramètre effectif lorsqu’il apparait dans l’appel de la fonction
Dans la cellule précédente, la chaine de caractères
toto
est le paramètre effectif de l’appel de la fonctionprint()
.print()
est une fonction prédéfinie compliquée qui possède de nombreux paramètres formels.
#help(print)
1.1.2. Définir une nouvelle fonction#
Soit la fonction affine \(f(x) = 2x + 1\). On veut calculer \(f(0)\), \(f(1)\), \(f(-1)\) ou tracer sa représentation graphique.
On va définir en python une fonction nommée f
qui réalise ce calcul.
def f(x : float) -> float:
res = 2 * x + 1
return res
def
annonce la définition de la fonctionf
on écrit aussi
f()
qui explicite quef
est une fonction
les paramètres de la fonction sont entre parenthèses
( )
f()
possède un seul paramètrex
de typefloat
. Noter le symbole:
dans les( )
Ainsi
x
est le paramètre formel de la fonctionf()
On
f()
retourne une valeur de typefloat
. Noter le symbole->
après les( )
cette première ligne se termine par un
:
qui annonce une partie indentéela partie indentée par rapport à
def
définit le traitementon reconnaît le calcul de \(f(x)\)
le résultat de ce calcul est affecté dans
res
:res
est une variable locale à la fonction
return
désigne la valeur retournée par la fonctionici la valeur de
res
est retournée parf()
A retenir
Le mot clé
def
, les()
et le:
Les types des paramètres formels et de la valeur retournée : les
:
dans les( )
et le->
1.1.3. Appeler cette fonction#
Exécuter la cellule qui contient la définition de la fonction f()
ne calcule rien. Pour “calculer avec f”, il faut appeler la fonction f()
pour des valeurs de son paramètre x
.
# 3 appels pour des valeurs numériques
a = f(0)
b = f(1.0)
c = f(-1)
d = f(3.0)
print(a, b, c, d)
1 3.0 -1 7.0
# 2 appels pour des variables
zero = 0
un = f(zero)
trois = f(un)
print(zero, un, trois)
0 1 3
# 2 appels dans une expression
combien = f(zero) + f(un)
# 2 appels : 1 d'une expression et 1 dans un argument qui est une expression
et_celui_la = f(zero + f(un))
print(combien, et_celui_la)
4 7
# un joli tracé vaut mieux qu'un long discours
import matplotlib.pyplot as plt
x = [i for i in range(-5,5)] # une liste de 10 arguments
y = [f(i) for i in x] # la liste des 10 valeurs de la fonction correspondante
plt.plot(x, y)
plt.title("f(x)=2x+1")
#plt.close()
Text(0.5, 1.0, 'f(x)=2x+1')

Danger
NE PAS CONFONDRE : LA définition vs. LES appels
1.1.4. Tester cette fonction#
Il faut s’assurer que l’implémentation effectue sans erreur le traitement voulu. Pour celà, on effectue des tests unitaires basés sur des propriétés caractéristiques de la fonction.
L’instruction assert
de python est utile à cet effet.
Exemple. Une implémentation correcte des fonctions trigonométriques \(cos\) et \(sin\) doit vérifier pour toute valeur réelle \(x\), \(cos(x)^2 + sin(x)^2 = 1\).
Cette expression est une assertion, c-a-d. une expression qui est vraie.
from math import sin
c = cos(pi/3.0)
s = sin(pi/3.0)
assert c*c + s*s == 1
Il ne se passe rien. L’assertion est vraie. Morale : “Pas de nouvelle, bonne nouvelle !”
On peut donc être un peu plus exhaustif en testant d’autres valeurs dont on connait le résultat attendu.
# des valeurs caractéristiques
assert cos(0) == 1.0
assert cos(pi) == -1.0
assert sin(0) == 0.0
assert sin(pi/2.) == 1.0
Aucune erreur n’est signalée, ce qui est rassurant. Au moins dans un premier temps : nous avons vu plus haut que avec le calcul de cos(pi/3)
souffrait d’une erreur à la 16ème décimale.
cos(pi/3)
0.5000000000000001
On peut donc craindre de perdre certaines égalités pour certaines valeurs de \(x\). Dans ces cas, les tests unitaires déclancheront une erreur. Par exemple :
assert cos(pi/2.) == 0.0
provoquera l’affichage suivant :
AssertionError
Traceback (most recent call last)
Cell In[12], line 1
----> 1 assert cos(pi/2.) == 0.0
AssertionError:
(\(\star\)) Que penser de ces “résultats faux” ? Rassurez-vous : vous pouvez faire confiance aux résultats de ces fonctions élementaires. Il n’y a pas de bug. Cette petite imprécision est inévitable : il est par exemple impossible de représenter la valeur numérique exacte de \(\pi\) sur un ordinateur – et aussi sur votre feuille. Il en est de même pour \(\pi/2\) et donc ce serait chanceux que la fonction \(\cos\) en calcule le résultat exact – sauf à être elle-même fausse ou très savante ! Ce type de problème relève de l’analyse des erreurs d’arrondi des calculs sur ordinateur. Ce qui dépasse le cadre de ce chapitre.
Morale. Toujours prendre avec précaution le résultat d’un calcul avec des nombres flottants car chaque opération arithmétique peut introduire une petite erreur, parfois – mais pas toujours – sans grande conséquence.
# une propriété qui ne rassure pas complètement ...
import random
for _ in range(20):
v = random.uniform(-10,10)
#assert cos(v)**2 + sin(v)**2 == 1
print(cos(v)**2 + sin(v)**2)
1.0
1.0
1.0
0.9999999999999999
1.0
1.0
1.0
1.0
1.0
1.0
1.0
0.9999999999999999
1.0
0.9999999999999999
1.0
1.0
1.0
1.0
1.0
1.0
A retenir
Toujours tester les fonctions que vous définissez
En pratique, je teste une fonction avec l’instruction assert
et pas avec print()
Des tests unitaires, même en grand nombre, ne vous permettent pas de conclure que votre implémentation est correcte. Mais juste que vous n’avez pas réussi à exhiber un éventuel traitement faux.
1.1.5. Pourquoi des fonctions ?#
Les exemples précédents illustrent des premières réponses à cette question.
Eviter de ré-écrire, de dupliquer, un calcul, un traitement, du code. Ré-utiliser l’existant.
Simplifier la lecture donc la compréhension
Ecrire des algorithmes, des codes de façon plus modulaire
Modularité : découper un traitement compliqué en une suite d’étapes plus simples, plus indépendantes. Une étape peut être réalisée par une fonction. Une étape peut être elle-même découpée jusqu’à obtenir un traitement assez simple pour l’écrire !
1.2. Définition, signature, corps et appels de fonction#
Le vocabulaire des fonctions varie selon les langages de programmation. Reprenons certains des aspects déjà décrits avec d’autres notions proches et très classiques en programmation.
1.2.1. Distinguer le Quoi du Comment#
De façon imagée, une fonction peut-être vu comme :
une boîte opaque
qui “prends des choses” en entrée : les paramètres,
qui effectue des traitements dépendants de ces paramètres : la zone indentée,
et qui fourni un “résultat” en sortie : le
return
.
La boîte porte le nom de la fonction et est en général accompagnée d’une description de ce qu’elle fait.
La boîte est opaque. Je peux m’en servir – je sais ce qu’elle fait – mais pour ça, je n’ai pas besoin de savoir comment elle le fait.
Exemple. Vous savez ce que vaut cos(0)
mais a priori pas comment il est calculé.
On distingue ainsi de :
la spécification d’une fonction = ce que ça fait : le QUOI
une implémentation de cette fonction = comment ça le fait : le COMMENT
Warning
Vocabulaire – selon les langages de programmation :
spécification = signature = en-tête (= prototype)
implémentation = corps
1.2.2. La signature d’une fonction en python#
La signature d’une fonction explicite le minimum nécessaire et suffisant pour appeler cette fonction, ie. le quoi : que fait la fonction et de quoi a-t-elle besoin pour ça ?
On ne gardera que le nécessaire de la syntaxe de la définition vue jusqu’ici.
def f(x : float) -> float:
'''Fonction affine de coeff directeur 2 et d'ordonnée à l'origine 1'''
La lecture de cette signature indique que la fonction f
prend en entrée un paramètre de type numérique à virgule flottante (un flottant) noté x
et fourni en retour une valeur flottante.
Le commentaire, appelé docstring (ou zone de documentation) décrit ce que fait la fonction f
.
Danger
ATTENTION : cette seule signature ne définie pas une fonction exécutable en python
En revanche, cette signature suffit à écrire les tests unitaires qui seront exécutés une fois la fonction entièrement définie.
1.2.3. Le corps et le retour de valeur en python#
Le corps définit le traitement effectué par la fonction, ie. l’implémentation de la fonction.
Il complète la signature.
Ce traitement s’effectue avec les paramètres formels et des variables locales à la fonction.
Ce traitement comporte en général une valeur de sortie retournée par l’instruction return
.
Rappels :
le corps est indenté d’un niveau par rapport à celui du
def
(qui finit par un:
).mot-clé
return
return
une valeur, une variable, une expressionreturn None
: ne retourne rien … mais le dit !l’exécution d’un
return
termine l’exécution du corps de la fonctionà la fin de l’écriture du corps, en général
mais pas toujours : le
return
n’est pas nécessairement unique
On retrouve donc la définition complète d’une fonction, maintenant décomposée en sa signature et son corps.
def f(x : float) -> float:
'''Une fonction affine déjà rencontrée'''
y = 2 * x + 1
return y
1.2.4. L’appel d’une fonction en python#
L’appel de la fonction consiste à exécuter la fonction pour des valeurs fixées des arguments d’entrée.
En python :
le nom de la fonction : sans le
def
mais avec les( )
les paramètres effectifs : les valeurs des arguments d’entrée
les identifiants des variables de sortie
Exemple (déjà vu)
##### 3 appels pour des valeurs numériques
a = f(0)
assert a == 1
assert f(1.0) == 3
zero = 0
un = f(zero)
assert a == un
combien = f(zero) + f(un)
assert combien > un
assert combien < f(2)
print(combien)
4
Danger
NE PAS CONFONDRE : paramètre formel vs. paramètre effectif ou argument
1.2.5. Méthodologie d’écriture des fonctions pour ce semestre#
En pratique, vous respecterez l’ordre d’écriture suivant.
Important
écriture de la signature de la fonction
docstring compris
écriture d’appels simples, de tests unitaires
rappel : appels sur des valeurs de tests, cad. dont on connait le résultat attendu de la fonction
et correction de l’étape 1 si besoin
écriture du corps
exécution des tests unitaires
et correction de l’étape 2 si besoin
écriture des appels voulus
exécution des appels voulus
Tip
CONSIGNE PEDAGOGIQUE : Ecrire l’appel (simple) ou les tests unitaires avant d’écrire le corps, ie. en se basant uniquement sur la signature.
Ainsi :
une erreur de spécification peut être détectée : absence ou excès de paramètre, ordre des paramètres mal choisi, …
il n’est pas possible d’exécuter cet appel avant d’avoir fini l’écriture du corps.
# définition de 3 fonctions
def doubler(u : int) -> int:
""" retourne le double de u """
return 2 * u
def tripler(u : int) -> int:
""" retourne le triple de u """
v = 3 * u
return v
# des tests unitaires
assert doubler(0) == 0
assert tripler(0) == 0
for i in range(-5,5):
assert doubler(i) == i + i
assert tripler(i) == i + i + i
from random import uniform
for i in range(10):
x = uniform(-1, 1)
assert doubler(x) == x + x
assert tripler(x) == x + x + x
# des appels
d = doubler(1234)
t = tripler(5678)
# un affichage
print(d, t)
2468 17034
Danger
INTERDIT PEDAGOGIQUE : il est interdit d’utiliser l’identifiant du paramètre formel comme paramètre effectif d’un appel.
x = 3
# L'appel suivant est interdit pour la fonction affine f(x) définie précédemment
y = f(x)
y = f(3) # ok
val = 3
y = f(val) # ok
A retenir
L’appel est situé après la définition
logique : il faut connaître la fonction pour l’utiliser !
python : se souvenir que
from module import f
permet de connaître, donc d’utiliser,f
qui a été définie (par moi ou par un.e autre) dans la bibliothèquemodule
1.3. L’appel de fonction rompt le séquencement des instructions écrites dans le code source#
Le modèle d’exécution séquentielle des instructions consiste à exécuter l’instruction qui suit celle qui vient de s’exécuter. Mais de quel ordre parle-t-on ?
L’ordre des instructions exécutées n’est pas l’ordre des instructions écrites dans le code source (python).
Les cas le plus simple est une suite d’instructions composée d’affectations, d’opérateurs booléen ou arithmétiques ou encore d’expressions arithmétiques. Ici les instructions sont globalement exécutées “ligne après ligne”, pour ne pas dire “instruction après instruction”.
Les structures de contrôle cassent ce séquencement “ligne après ligne” du code source.
Les tests (
if .. elif .. else
) engendrent des sauts vers des lignes du code source distantes de celle du test.Les répétitions (
for ..
ouwhile ..
) engendrent des retours vers des lignes du code source placées avant la dernière exécutée.
Ainsi tests et répétitions rompent le séquencement des instructions écrites dans le code source. Les fonctions génèrent un ordre d’exécution encore différent.
Nous illustrons avec pythontutor l’exécution d’un appel de fonction.
Fonction affine : visualisation pythontutor
On observe :
l’appel de fonction provoque une rupture de l’exécution séquentielle,
il provoque un “débranchement” de l’appelant vers l’appelé ;
le
return
provoque le retour au cadre appelant pour continuer l’exécution séquentielle.Plusieurs
return
?Oui mais 1 seul exécuté par appel.
1.3.1. Appel = appelant \(\to\) appelé ; return
= appelé \(\to\) appelant#
Distinguer appelant et appelé :
l’appelant : le cadre (ie. la zone logique de code) où l’appel est effectué
l’appelé : le traitement de la définition de la fonction avec les paramètres effectifs de l’appel
L’appel provoque une rupture de l’exécution séquentielle de l’appelant : un débranchement de l’appelant vers l’appelé.
L’appel “passe la main” à l’appelé en même temps qu’il transmet les paramètres effectifs et une information pour que l’appelé puisse, plus tard, revenir dans l’appelant (pour fournir le résultat, ie. retourner le résultat).
L’exécution revient dans la zone de l’appelant une fois achevée l’exécution de l’appelé – en pratique, après l’exécution d’un
return
.Ce
return
déclenche le retour dans l’appelant à l’instruction de l’appel – car c’est souvent une affectation si l’appelé est une (vraie) fonction – ou à l’instruction suivante – si c’est une procédure qui ne renvoie aucun résultat, cad. un appel sans affectation.
A retenir
l’appel = appelant -> appelé
le
return
= appelé -> appelant
1.3.2. Le return
termine l’appel de la fonction#
L’exécution d’un return
, quel qu’il soit, termine l’exécution de la fonction – et provoque le retour dans le cadre appelant.
Plusieurs instructions return
sont possibles dans une même fonction.
Donc pas uniquement comme dernière instruction du corps de la fonction.
Le premier
return
exécuté arrête l’exécutionles instructions suivantes du corps de la fonction ne sont pas traitées – comme dans une branche non prise d’un
if .. elif .. else ..
.
visualisation pythontutor
de plusieurs return
et 1 seul exécuté par appel.
def abs0(u : float) -> float:
'''calcule la valeur absolue de u'''
if u > 0:
res = u
else:
res = -u
return res
Rmq.
res
est une variable locale à la fonction qui est définie pour retourner le résultat du traitement de la fonction.
def abs2(u : float) -> float:
'''La même avec 2 return '''
if u > 0:
res = u
return res
else:
res = -u
return res
# 2 appels
a_x = abs0(2)
a_y = abs0(-5)
# affichage
print(a_x, a_y)
# intérêt du docstring
help(abs0)
help(abs2)
2 5
Help on function abs0 in module __main__:
abs0(u: float) -> float
calcule la valeur absolue de u
Help on function abs2 in module __main__:
abs2(u: float) -> float
La même avec 2 return
def abs2bis(u : float) -> float:
'''La même avec 2 return sans variable locale'''
if u > 0:
return u
else:
return -u
Nous verrons que les fonctions récursives sont friandes de return
très tôt écrits dans le corps.
1.3.3. Exercice#
Reprendre les fonctions précédentes (qui calculent et retournent la valeur absolue du paramètre d’entrée) et bien identifier le séquencement des instructions exécutées dans l’appelant et l’appelé.
1.4. Spécifier le type des paramètres d’une fonction#
Avant d’indiquer comment spécifier des paramètres formels de type list
, rappelons comment procéder pour les types scalaires.
En python, il s’agit d’annotations de typage.
Important
Evolutions des annotations de typage
python \(<\) 3.9 : module
typing
nécessaire pour définirList[...]
avec une majusculepython \(\ge\) 3.9 : possibilité d’utiliser le type
list[...]
sans majuscule et sans importer le moduletyping
python \(\ge\) 3.10 : le symbole
|
pour désigner l’union de types (exUnion
)
Note
La suite de ce chapitre utilise les possibilités de python \(\ge\) 3.9`
Syntaxe des annotations
Dans la définition de la fonction, c-a-d. def f() ...
paramètres entre
( )
:identifiant_du_paramètre : type_du_paramètre
paramètre de retour :
-> type_du_paramètre
Les mots-clés des types de paramètres sont explicites : int
, float
, str
, bool
Exemples.
def prixTotal(nbUnités : int, prixUnitaire : float) -> float
def nbVoyelles(mot: str) -> int
def assertion(s : str) -> bool
1.4.2. Comment spécifier une fonction avec des paramètres formels de type tableau ?#
Pour chaque paramètre formel de type tableau (représenté par une liste python), on complète la spécification de la fonction avec autant de paramètres nécessaires à la définition de la dimension du tableau : un paramètre supplémentaire pour un tableau 1D, deux pour un tableau 2D, …
Montrons avec des exemples comment préciser la (ou les) taille(s) fixe(s) d’un tableau.
Exemple :
la fonction min1()
doit retourner la valeur minimale d’un tableau 1 D t
, d’entiers (par exemple) et de taille n
quelconque.
La spécification de cette fonction s’écrira :
def min1(t: list[int], n: int) -> int:
'''retourne la valeur minimale du tableau d'entiers t de taille n'''
La taille
n
est définie ici comme second paramètre de cette fonction.C’est bien un entier
Ce paramètre supplémentaire n’est pas nécessaire en python mais il est obligatoire dans cet enseignement
Nous étendons cette pratique aux tableaux multidimensionnels.
Par exemple, la spécification de la fonction qui identifie et retourne la valeur minimale dans un tableau 2D s’écrit par exemple (pour un tableau 2D d’entiers) :
def min2(t: list[list[int]], n: int, m: int) -> int:
'''retourne la valeur minimale du tableau 2D d'entiers t de taille n x m'''
Remarquons :
l’annotation du type de
t
, paramètre formel de “type tableau 2D”, comme une liste de listes d’entiers,et les 2 paramètres de taille
n
etm
, qui désignent respectivement le nombre de lignes et de colonnes det
.
Important
Convention à appliquer ce semestre
Lorsqu’une fonction admet un paramètre formel de type tableau (ou plus d’un), on convient que les paramètres de taille de chaque paramètre tableau suivent sa définition dans la spécification de la fonction.
La seule simplification possible est la suivante : si la fonction admet plusieurs paramètres de type tableau de même taille, alors ces paramètres sont placés en dernier, suivis d’une seule définition de leur taille.
Exemples.
si il y a 1 paramètre tableau 1D, on écrit dans l’ordre les types de
tab1, taille_de_tab1
si il y a 2 paramètres tableaux 1D, on écrit dans l’ordre les types de
tab1, taille_de_tab1, tab2, taille_de_tab2
si il y a 1 paramètre tableau 2D, on écrit dans l’ordre les types de
tab1, nb_lignes_de_tab1, nb_colonnes_de_tab1
,
Exemple de la simplification.
(\(\star\)) On en profite pour montrer comment construire une pré-condition avec un assert
.
def somme3vecteurs(v1: list[int], v2: list[int], v3: list[int], n: int) -> list[int]:
'''Calcule et retourne la somme des 3 vecteurs v1, v2 et v3 de même taille n'''
assert len(v1) == len(v2) == len(v3) == n
1.4.3. \(\star\) Sur les annotations de fonctions#
Le type des paramètres de fonctions sont des annotations en python. Ces annotations sont optionnelles. Ainsi, un appel qui ne respecte pas le type des paramètres ne provoquera pas d’erreur. python appliquera un typage dynamique : la valeur définit le type de la variable affectée. Ainsi le traitement de la fonction pourra, selon l’incompatibilité, provoquer ou non une erreur à l’exécution.
D’autres langages de programmation sont moins permissifs et signalent ces erreurs de type à la compilation. Des outils python effectuent ce contrôle de types : mypy par exemple.
Exemple.
v = doubler(5.5)
s = doubler("bonjour")
print(v, s)
11.0 bonjourbonjour
1.5. Compléments#
1.5.1. Ne pas confondre return
et print
#
C’est une erreur très fréquente hélas.
La relecture de paragraphe sur le rôle du return
permet de bien se convaincre qu’un return
réalise un traitement bien différent d’un affichage – ce qui est le rôle du print
.
Important
CONSEIL DÉJÀ SOUVENT RAPPELÉ : séparer les E/S des traitements
En pratique, les E/S (print
et input
) doivent être limitées :
au programme principal : le
main
, l’appelant de plus haut niveauaux fonctions d’entrées-sorties spécifiques à votre application, si il y en a.
Voir
afficher_pref_dpt()
en fin de chapitre.
1.5.2. help()
et docstring#
help()
est une fonction python prédéfinie qui affiche l’en-tête (la signature) complet de la fonction passée en paramètre.
Elle fournit les informations nécessaires à l’utilisation correcte de la fonction, ainsi que des détails sur son objet et son implémentation – si le ces aspects ont été décrits par le programmeur dans le docstring
: la zone de commentaires entre '''
(3 quotes ou """
(3 double quotes) et indentée sous le def
.
Cette fonction est similaire à l’option de commande --help
sous Unix.
help(doubler)
Help on function doubler in module __main__:
doubler(u: int) -> int
retourne le double de u
help(f)
Help on function f in module __main__:
f(x: float) -> float
Une fonction affine déjà rencontrée
Exercice. A quoi reconnait-on que help
est une fonction ?
1.5.3. Plusieurs paramètres#
def puissance(x : float, n : int) -> float:
'''calcule x**n de façon itérative pour'''
# (un) corps de la fonction puissance
r = 1.0
for i in range(1, n+1):
r = r * x
return r
# tests
assert puissance(1.0, 0) == 1.
assert puissance(1.0, 1) == 1.
assert puissance(2.0, 3) == 8.0
# appels avec des valeurs
mille = puissance(10.0, 3)
print("mille =", mille)
# si besoin, avant d'écrire un assert
vrai_ou_faux = (puissance(3.1,2) == 3.1 * 3.1)
print(vrai_ou_faux)
mille = 1000.0
True
# appels avec une variable
# évitons les entrées au clavier
#n = int(input("n premières puissances de 2.0 pour n = "))
n = 5
for i in range(n+1):
print("2 **", i, "=", puissance(2, i))
print()
# évitons les entrées au clavier
#p = float(input("n premières puissances de p pour p = "))
#n = int(input("et n = "))
p = 3
n = 4
for i in range(n+1):
print(puissance(p, i))
2 ** 0 = 1.0
2 ** 1 = 2.0
2 ** 2 = 4.0
2 ** 3 = 8.0
2 ** 4 = 16.0
2 ** 5 = 32.0
1.0
3.0
9.0
27.0
81.0
1.5.4. Fonction sans paramètre#
Une fonction peut ne pas avoir de paramètre formel. Et donc être appelée sans argument (sans paramètre effectif).
def tetu() :
return 1
res = tetu() # appel et affectation du résultat
print('res = ', res) # affichage
# boucle d'appels à tetu() dans un print()
for i in range(5):
print('appel tetu', tetu())
# appel sans affectation, ni print.
# L'interpréteur python affiche sa valeur en Out[..]
tetu()
res = 1
appel tetu 1
appel tetu 1
appel tetu 1
appel tetu 1
appel tetu 1
1
1.5.5. Fonction sans return
#
Warning
Toute fonction python retourne un résultat. Si aucun return
est exécuté, la valeur None
est systématiquement retournée.
Exemple
def mal_ecrite(x : int) -> int:
'''Si si je suis un bon exemple de fonction sans return'''
if x > 0:
return x
a = mal_ecrite(1)
b = mal_ecrite(0)
print(a, b)
1 None
1.5.6. Portée des variables, variable locale, variable globale,#
La portée d’une variable : la zone où une variable est accessible, c-a-d. (re)connue à l’aide de son identifiant.
Les notions de variable locale (à une fonction) ou de variable globale sont classiques en programmation.
Variable locale à une fonction : variable définie dans le corps de la fonction pour permettre le traitement réalisé par la fonction.
Par extension, les paramètres formels de la fonction sont aussi des variables locales à sa définition.
Une variable est dite globale par rapport à une fonction si elle est définie à l’extérieur de la fonction, et plus particulièrement dans l’appelant de la fonction.
Convention. Sauf besoin, on utilisera le terme “variable locale” sans ajouter “locale à une fonction”.
La portée d’une variable locale est limitée au corps de la fonction qui contient sa définition.
- Une variable locale n’existe pas à l’extérieur de la définition ou du traitement de la fonction.La portée d’une variable globale est définie par le cadre appelant.
La portée d’une variable commence à sa définition et s’étend au cadre qui contient cette définition.
cette portée inclut les fonctions définies dans ce cadre et aussi leurs corps
Les portées d’une variable globale et d’une variable locale peuvent se recouvrir (dans la fonction de la variable locale) si ces 2 variables ont le même identifiant.
Cette dernière remarque justifie les deux interdits suivants.
Danger
INTERDITS PEDAGOGIQUES
Dans la définition d’une fonction, il est interdit d’introduire ou d’utiliser des variables globales, excepté si ce sont des constantes.
Il est interdit d’utiliser l’identifiant du paramètre formel comme paramètre effectif d’un appel.
Ainsi en pratique, vous limitez le corps d’une fonction au traitement de ses paramètres formels et de variables locales.
(\(\star\)) On comprend maintenant la justification de cet interdit déjà posé.
A retenir
CONVENTION PYTHON : les identifiants de constantes s’écrivent en MAJUSCULES
ANNEE_EN_COURS = 2020
Exemple.
Reprendre le pythontutor précédent et observer comment
res1
oures2
naissent, vivent et meurent.
Au delà de certaines spécificités de python qui la rendent assez délicate (un chapitre sera dédié à cet aspect), l’utilisation de variables globale est à l’origine de la notion d’effet de bord souvent à l’origine de longues phases de debugging.
Si on souhaite utiliser une variable extérieure à une fonction, il suffit de la passer commme paramètre de cette fonction.
Exercice.
Reprendre les fonctions
abs0
,abs2
etabs2bis
précédentes et identifier leurs variables locales et leur portée, dans la définition, dans les appels, dans les appelants.
1.5.7. Fonction locale#
A la manière d’une variable locale, une fonction locale (à une fonction) est :
définie dans le corps d’une fonction,
puis utilisable par cette fonction (englobante)
mais uniquement par elle : elle est inconnue de l’extérieur de la fonction englobante
et ce récursivement.
def max3(x : int, y : int, z : int) -> int:
'''
autre version (1 return) sans fonction locale
role : calcule et retourne le max de 3 valeurs
'''
if x < y:
if y < z:
res = z
else:
res = y
else:
if x < z:
res = z
else:
res = x
return res
# test exhaustif pour 3 valeurs différentes
print(max3(1, 4, 2), max3(1, 2, 4), max3(2, 1, 4), max3(2, 4, 1), max3(4, 1, 2), max3(4, 2, 1))
4 4 4 4 4 4
def max3_f_loc(x : int, y : int, z : int) -> int:
'''
role : calcule et retourne le max de 3 valeurs
avec fonction locale
'''
def max2(u, v : int) -> int:
'''
fontion locale :
calcule et retourne le max de 2 valeurs'''
if u > v:
res = u
else:
res = v
return res
if x < y:
res = max2(y, z)
else:
res = max2(x, z)
return res
m1 = max3(1, 4, 2)
m2 = max3_f_loc(1, 4, 2)
print(m1 == m2)
True
Exercice.
Compléter le test unitaire de
max3
.(\(\star\)) Ecrire une version de
max3
sans variable locale, ni fonction locale.(\(\star\)) Ecrire une version de
max3
avec une fonction locale et réduite à une seule ligne d’instruction (pour le traitement demax3
).
Solutions.
def max3_v2(x : int, y : int, z : int) -> int:
'''
role : calcule et retourne le max de 3 valeurs
version 4 return : sans variable, ni fonction locale
'''
if x < y:
if y < z:
return z
else:
return y
if x < z:
return z
else:
return x
# exemple de tests unitaires à partir d'une version de référence
from random import randint
for i in range(10):
x = uniform(-50, 50)
y = uniform(-50, 50)
z = uniform(-50, 50)
assert max3(x, y, z) == max3_v2(x, y, z)
def max3_fonctionnelle(x : int, y : int, z : int) -> int:
'''
role : calcule et retourne le max de 3 valeurs
'''
def max2(u, v : int) -> int:
'''
fonction locale :
calcule et retourne le max de 2 valeurs'''
if u > v:
res = u
else:
res = v
return res
return max2(x, max2(y, z))
m = max3_fonctionnelle(1, 4, 2)
print(m)
4
1.5.8. Pas d’effet de bord ! Pas de global
!#
Une fonction réalise un effet de bord si son traitement modifie (au moins) une variable de l’appelant qui n’est pas la variable d’affectation de son retour, ni un paramètre effectif de l’appel (comme nous le verrons dans un prochain chapitre).
En pratique (en python), l’éventuel effet de bord peut concerner n’importe qu’elle variable de l’appelant qui existe avant l’appel de la fonction, ie. hors du cadre de la fonction, dès lors que cette variable est caractérisée comme (pour variable globale).
Danger
Pas d’effet de bord implique donc en python pas d’utilisation du mot-clé global
.
Exemple.
Dans l’exemple suivant, le second appel modifie aussi la valeur de a
. Ce qui est impossible à deviner sans connaître le corps de la fonction. On ne peut donc plus l’utiliser comme une “boîte noire”.
a = 0
b = 0
un = 1
def incremente(u : int) -> int:
return u + 1
def incremente_avec_effet_de_bord(u : int) -> int:
global a
a = a + 10 # mais pourquoi faire ça ici ...
return u + 1
a = incremente(un) # seul `a` est modifié par la fonction
print(a, b)
b = incremente_avec_effet_de_bord(un) # `a` et `b` sont modifiés
print(a, b)
2 0
12 2
1.5.9. \(\star\) Statique vs. dynamique, trace d’exécution#
Selon les langages de programmation, la signature peut-être séparée, voire compilée, indépendamment du corps de la fonction. Ainsi connue du compilateur, cette signature peut lui servir pour vérifier automatiquement la correction de l’implémentation de la fonction et aussi des appels à cette fonction : nombre de paramètres effectifs, type de ces paramètres, type du résultat retourné, … Ces vérifications sont possibles lors de la compilation , cad. avant toute exécution du programme. Ce type de vérification diminue le risque d’erreur lors de l’exécution.
On parle d’analyse statique (ou de vérification statique) pour ces traitements effectués lors de la compilation.
De façon complémentaire, on parle d’analyse dynamique (ou de vérification dynamique) pour les traitements effectués lors de l’exécution du programme.
Note
Par extention, le statique caractérise ce qui relève du code source tandis que le dynamique traduit ce qui est propre à l’exécution de ce code.
A code source constant, les instructions exécutées dépendent des paramètres de l’exécution. On appelle trace d’exécution l’ensemble des valeurs et des états produits par une exécution. Ainsi, le même code statique engendrera des traces d’exécutions différentes. On a déjà remarqué que la séquentialité des instructions écrites dans le code source et une trace de leur exécution sont différentes. La trace est une notion dynamique.
Remarque. Oui : un compilateur est un programme qui s’exécute ! Il “prend en entrée” un code (dit) source et produit en sortie un code (dit) exécutable. Ce traitement comporte plusieurs étapes dont celle de vérification mentionnée qui s’effectue lors de l’étape (dite) d’analyse syntaxique – ne pas confondre les termes “syntaxique” et “statique”.
1.5.10. A venir#
Un point important a été passé sous silence dans ce chapitre :
comment s’effectue le passage de paramètres lors de l’appel ?
Autrement dit :
comment le traitement défini sur les paramètres formels dans la définition de la fonction s’applique aux paramètres effectifs de l’appel ?
comment les paramètres formels de la définition “deviennent-ils” les paramètres effectifs de l’appel ?
Cette question importante en pratique est traitée dans le chapitre {ref}(ch:affectation).
1.6. Exercices en démonstration#
1.6.1. Différentes écritures de fonctions min#
Ecrire la fonction min(a,b) en suivant la méthodologie demandée
Proposer plusieurs écritures du corps : avec un seul return, avec deux, …
S’en servir pour écrire la fonction min(x, y, z) : signature, appels, puis corps
Proposer plusieurs écritures du corps
def min(a : int, b : int) -> int:
'''retourne min(a,b)
-> version un seul return
'''
if a < b:
m = a
else:
m = b
return m
def min2(a : int, b : int) -> int:
'''retourne min(a,b)
-> version deux return
'''
if a < b:
return a
else:
return b
def min3a(a : int, b : int, c : int) -> int:
'''retourne min(a,b,c)
-> version 1 return, 2 appels à min(a,b)
'''
if min(a, b) < c:
m = min(a, b)
else:
m = c
return m
def min3b(a : int, b : int, c : int) -> int :
'''retourne min(a,b,c)
-> version 1 return, 1 appel à min(a,b)
'''
m_ab = min(a, b)
if m_ab < c:
m = m_ab
else:
m = c
return m
def min3(a : int, b : int, c : int) -> int:
'''retourne min(a,b,c)
-> version fonctionnelle : 2 appels imbriqués
'''
return min(min(a, b), c)
# des appels
x = 23
y = 44
z = 27
m_xyz = min3a(x, y, z)
print(m_xyz)
m_xyz = min3b(x, y, z)
print(m_xyz)
m_xyz = min3(x, y, z)
print(m_xyz)
23
23
23
1.6.2. Une procédure d’affichage (E/S)#
Ecrire une fonction qui réalise efficacement l’affichage suivant :
Carcassonne est la préfecture du département 11
------------------------------------------------
Perpignan est la préfecture du département 66
------------------------------------------------
Montpellier est la préfecture du département 34
------------------------------------------------
Foix est la préfecture du département 09
------------------------------------------------
# Bourrin et pas satisfaisant
print("Carcassonne est la préfecture du département 11")
print("------------------------------------------------")
print("Perpignan est la préfecture du département 66")
print("------------------------------------------------")
print("Montpellier est la préfecture du département 34")
print("------------------------------------------------")
print("Foix est la préfecture du département 09")
print("------------------------------------------------")
# On pourrait bien sûr mettre tout dans un seul print (avec des `\n`)
# ce qui n'est pas satisfaisant non plus ...
Carcassonne est la préfecture du département 11
------------------------------------------------
Perpignan est la préfecture du département 66
------------------------------------------------
Montpellier est la préfecture du département 34
------------------------------------------------
Foix est la préfecture du département 09
------------------------------------------------
# Synthétique mais un peu pythonesque : une boucle sur des "couples" (tuple)
for v,d in ("Carca", "11"), ("Perpi","66"), ("Montpel", "34"), ("Foix", "09"):
print(v, "est la préfecture du département", d)
print("------------------------------------------------")
Carca est la préfecture du département 11
------------------------------------------------
Perpi est la préfecture du département 66
------------------------------------------------
Montpel est la préfecture du département 34
------------------------------------------------
Foix est la préfecture du département 09
------------------------------------------------
Le couple entre parenthèses
("Carca", "11")
est un tuple python, notion sur laquelle nous reviendrons très viteEn attendant, considérer ce tuple comme une constante composée de 2 valeurs de type string
# définition d'une fonction qui affiche ce qu'on veut comme on veut
def afficher_pref_dpt(ville : str, num_dpt : str):
print(ville, "est la préfecture du département", num_dpt)
print("------------------------------------------------")
# 4 appels de cette fonction
afficher_pref_dpt("Carca", "11")
afficher_pref_dpt("Perpi", "66")
afficher_pref_dpt("Montpell", "34")
afficher_pref_dpt("Foix", "09")
print()
# 4 appels synthétiques de façon pythonesque
for (v,d) in ("Carca", "11"), ("Perpi","66"), ("Montpell", "34"), ("Foix", "09"):
afficher_pref_dpt(v,d)
Carca est la préfecture du département 11
------------------------------------------------
Perpi est la préfecture du département 66
------------------------------------------------
Montpell est la préfecture du département 34
------------------------------------------------
Foix est la préfecture du département 09
------------------------------------------------
Carca est la préfecture du département 11
------------------------------------------------
Perpi est la préfecture du département 66
------------------------------------------------
Montpell est la préfecture du département 34
------------------------------------------------
Foix est la préfecture du département 09
------------------------------------------------
Rmq.
Pourquoi
afficher_pref_dept()
n’est pas une fonction excepté pour python ?
1.4.1. Comment indiquer le type d’un paramètre formel
list
?#Le type d’un tableau, représenté par une liste, sera décrit avec la syntaxe
list[type_val]
oùtype_val
est le type des valeurs du tableau.Exemples.
list[int]
: un tableau 1D d’entierslist[float]
: un tableau 1D de flottantslist[str]
: un tableau 1D de caractères ou de chaîne de caractères.list[list[int]]
: un tableau 2D d’entiers représenté par une liste de listesCette syntaxe permet d’expliciter le type (unique) des valeurs contenues dans un tableau.