IX. Les fonctions▲
IX-1. Généralités▲
Une fonction est un mécanisme permettant d’encapsuler un travail spécifique dans un bloc particulier identifié par un nom logique (nom de la fonction). Offrant ainsi à l’utilisateur la possibilité d'invoquer le bloc par son nom pour faire exécuter le travail encapsulé au moment de son choix.
Exemple : créer une fonction nommée salut qui affiche Hello
2.
3.
4.
5.
>>>
def
salut
(
):
... print
(
"Hello"
)
...
>>>
salut
(
) # Appel de la fonction (les parenthèses entrainent l'action)
Hello
L'instruction def
introduit une définition de fonction. Elle doit être suivie du nom de la fonction et de deux parenthèses ainsi que du fameux caractère « deux-points » (:) terminant systématiquement l’instruction définissant le bloc.
N’oubliez pas ensuite qu’il s’agit d’un bloc, donc les instructions de la fonction doivent être indentées pour bien indiquer leur appartenance à la fonction.
En dehors de son nom, deux autres catégories d'éléments caractérisent une fonction :
les diverses variables permettant de récupérer des informations envoyées par l’appelant et qui seront utilisées ensuite dans la fonction pour paramétrer son travail. Elles sont placées dans les parenthèses de la fonction et sont nommées les « paramètres » de la fonction. Et du côté appelant, les valeurs envoyées sont nommées « arguments ». Dans la théorie des langages, le nombre de paramètres et leurs types sont appelés « signature » de la fonction. Comme en Python le type d’une variable est défini à sa création, il n’existe pas donc pas de type spécifique pour un paramètre. La signature d’une fonction est donc simplement son nombre de paramètres ;
la valeur finale qu’elle produit (résultat) et qu’elle retourne à l’appelant qui l’attend pour continuer son travail. Elle sera effectivement renvoyée grâce à l’instruction
return
.
Exemple : une fonction qui additionne ses deux arguments reçus
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>
def
addition
(
a, b):
return
a+
b
...
>>>
calc=
addition
(
2
, 3
) # Appel de la fonction et récupération de son résultat
>>>
print
(
calc) # Affichage du résultat
5
>>>
calc=
2
*
addition
(
4
, 5
) # Utilisation du résultat de la fonction dans un calcul
>>>
print
(
calc) # Affichage du résultat du calcul
18
>>>
print
(
addition
(
6
, 7
)) # Affichage direct du résultat de la fonction
13
L’ordre des paramètres dans la fonction est important, car il conditionne l’ordre des arguments à passer lors de l’appel. Le premier argument ira dans le premier paramètre, le second argument ira dans le second paramètre, etc. On les nomme des « paramètres positionnels ».
Contrairement à d'autres langages, Python ne fait aucune différence entre « fonction » (qui renvoie une valeur) et « procédure » (qui ne renvoie rien). D'ailleurs, toute fonction renvoie toujours une valeur qui est par défaut None
si elle se termine sans avoir renvoyé autre chose.
Plusieurs return
sont possibles au sein d’une fonction. C’est le premier rencontré lors de l’exécution qui sera effectif. Cela permet de rendre une fonction plus modulable ou adaptative aux circonstances. Néanmoins, certains puristes préfèrent qu’il n’y ait qu’un seul return
par fonction, quitte à adapter (voire parfois alourdir) leur code pour arriver à ce résultat.
IX-2. Valeur par défaut des paramètres▲
Il est possible de proposer des valeurs par défaut pour des paramètres qui permettront alors à l’appelant d’omettre certains arguments.
Cela se fait avec l’opérateur « égal » qu’on rajoute au paramètre. Par exemple :
2.
3.
4.
5.
6.
7.
8.
def
ask_ok
(
prompt, essai=
1
, echec=
"Oui ou Non SVP !!!"
):
while
True
:
ok=
input(
prompt)
if
ok in
(
"y"
, "yep"
,"yeah"
, "yes"
): return
True
if
ok in
(
"n"
, "no"
, "nop"
, "nope"
): return
False
print
(
echec)
if
essai <=
1
: return
None
essai-=
1
Cette fonction doit être appelée avec impérativement un argument qui sera stocké dans le paramètre prompt. Les autres paramètres essai et echec peuvent être omis et dans ce cas, ils prendront la valeur spécifiée dans la définition de la fonction.
Exemple :
ask_ok
(
"Voulez-vous vraiment quitter ? "
)ask_ok
(
"OK pour écraser le fichier ?"
,2
)ask_ok
(
"OK pour écraser le fichier ? "
,2
,"Allez, seulement oui ou non !"
)
Attention : les paramètres ayant une valeur par défaut doivent être placés après les paramètres n’en ayant pas, car Python les remplira selon les positions des arguments passés lors de l’appel. De plus, si on omet un paramètre en lui faisant utiliser sa valeur par défaut, on est obligé d’omettre aussi tous ceux placés après, sauf si on les nomme explicitement (ce sera vu ultérieurement). Il est donc, par exemple, impossible d’appeler cette fonction en fournissant une valeur particulière pour le paramètre echec sans en fournir aussi une pour le paramètre essai.
De plus, la valeur par défaut d’un paramètre n’est créée en mémoire qu’une seule fois au moment où la fonction est créée et non au moment où elle est appelée :
2.
3.
4.
5.
6.
7.
8.
9.
>>>
i=
1
def
fct
(
j=
i): print
(
j)
...
>>>
fct
(
)
1
>>>
i=
2
>>>
fct
(
)
1
>>>
Cela peut alors devenir problématique lorsque cette valeur par défaut est un objet mutable tel qu’une liste, un dictionnaire ou des instances de la plupart des classes. Par exemple, la fonction suivante accumule les arguments qui lui sont passés au fil des appels successifs :
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
def
f
(
n, tab=
[]):
... tab.append
(
n)
... return
tab
...
>>>
f
(
1
)
[1
]
>>>
f
(
2
)
[1
, 2
]
>>>
f
(
3
)
[1
, 2
, 3
]
Pour corriger ce souci, il faut alors utiliser une valeur immuable comme valeur par défaut (la plus neutre et faite pour ça étant la valeur None
) ; et ensuite, tester cette valeur. Exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>
def
f
(
n, tab=
None
):
... if
tab is
None
: tab=
[]
... tab.append
(
n)
... return
tab
...
>>>
f
(
1
)
[1
]
>>>
f
(
2
)
[2
]
>>>
f
(
3
)
[3
]
IX-3. Les paramètres nommés▲
Les fonctions peuvent également être appelées en spécifiant explicitement dans quel paramètre doit aller tel argument, sous la forme parametre=
valeur. Dans ce cas, la position des arguments lors de l’appel n’a plus d’importance puisqu’on associe explicitement un argument à son paramètre.
La combinaison positionnel/nommé est possible à condition que les arguments nommés soient placés après les arguments positionnels.
Et, quelle que soit la façon d’appeler, il faut dans tous les cas veiller à ce que chaque paramètre ne soit rempli qu’avec un seul argument.
Par exemple, une fonction qui indique si p est divisible par q :
def
divisible
(
p, q=
1
): return
(
p%
q) ==
0
Cette fonction accepte un paramètre obligatoire (p) et un paramètre facultatif (q). Elle peut alors être appelée de n’importe laquelle des façons suivantes…
2.
3.
4.
5.
6.
divisible
(
5
, 2
) # deux arguments positionnels
divisible
(
5
, q=
2
) # un argument positionnel, un argument nommé placé après
divisible
(
5
) # un argument positionnel, un argument par défaut
divisible
(
p=
5
) # un argument nommé, un argument par défaut
divisible
(
p=
5
, q=
2
) # deux arguments nommé
divisible
(
q=
2
, p=
5
) # deux arguments nommés (l’ordre n’a alors pas d’importance)
… mais tous les appels qui suivent sont incorrects :
2.
3.
divisible
(
) # un argument obligatoire manquant
divisible
(
p=
5
, 2
) # un argument positionnel après un argument nommé
divisible
(
5
, p=
2
) # deux arguments pour le même paramètre "p"
IX-4. Verrouillage des méthodes de passage des arguments▲
Comme on vient de le voir, une fonction peut être appelée avec des paramètres positionnels ou des paramètres nommés au choix de l'appelant.
Cependant, depuis Python 3, le concepteur d'une fonction peut imposer à l'appelant de lui passer ses paramètres exclusivement par nom.
Cela se fait en positionnant le caractère étoile (*
) dans la liste des paramètres de la fonction. Tout paramètre placé après l'étoile sera impérativement nommé, et tout paramètre placé avant restera au choix de l'appelant.
Exemple :
>>>
def
fct
(
a, *
, b): print
(
a, b)
Cette fonction laisse le choix libre pour le paramètre a et impose que le paramètre b soit explicitement nommé lors de l'appel. Elle peut alors être appelée de n’importe laquelle des deux façons suivantes…
2.
3.
4.
>>>
fct
(
1
, b=
2
)
1
2
>>>
fct
(
a=
1
, b=
2
)
1
2
… mais l’appel suivant est incorrect :
>>>
fct
(
1
, 2
) # Le 2° argument n'est pas nommé
En plus de cette première possibilité, il a été rajouté dans Python 3.8 une seconde syntaxe pour imposer aussi un passage par position.
Cela se fait en positionnant le caractère slash (/
) tout en laissant la possibilité de placer aussi le caractère étoile (*
) dans la liste des paramètres de la fonction. Tout paramètre placé avant le slash sera impérativement positionnel, tout paramètre placé après l'étoile sera impérativement nommé, et tout paramètre placé entre les deux (ou après le premier ou avant le second s'il n'y en a qu'un seul) restera au choix de l'appelant.
Exemple :
>>>
def
fct
(
a, /
, b, *
, c): print
(
a, b, c)
Cette fonction demande que son premier paramètre a soit donné par position, laisse le choix libre pour le paramètre b et impose que le paramètre c soit explicitement nommé lors de l'appel. Elle peut alors être appelée de n’importe laquelle des deux façons suivantes…
2.
3.
4.
>>>
fct
(
1
, 2
, c=
3
)
1
2
3
>>>
fct
(
1
, b=
2
, c=
3
)
1
2
3
… mais les deux appels qui suivent sont incorrects :
2.
>>>
fct
(
a=
1
, b=
2
, c=
3
) # Le 1er argument n'est pas positionnel
>>>
fct
(
1
, 2
, 3
) # Le 3° argument n'est pas nommé
Bien évidemment, le concepteur de la fonction peut, à sa discrétion, ne verrouiller qu'une des deux formes d'appel (en n'utilisant que le slash ou que l'étoile), ou aucune.
IX-5. Les annotations▲
Les annotations déjà vues pour les variables de base sont aussi possibles pour les paramètres (qui ne sont eux aussi que des variables) avec les mêmes règles et la même syntaxe.
De plus, l’annotation est aussi possible pour la fonction permettant alors d’indiquer le type de valeur renvoyée. Cela se fait en rajoutant ‑>
nom_du_type_renvoyé avant les deux‑points terminant la fonction.
Exemple :
Syntaxe habituelle Sélectionnez 1. 2.
|
Comme pour les variables, cette notation est facultative, mais si elle est appliquée, le typage indiqué doit correspondre à la même liste int, float, bytes, str, bool, tuple, list, set, dict, None
, callable, object et any ; plus les éventuels noms des objets personnels (sera vu ultérieurement) créés en amont dans le code.
Et comme pour les variables, ces indications sont, pour l’instant, purement informatives et non contrôlées.
IX-6. Les paramètres supplémentaires facultatifs▲
Après les paramètres classiques, on peut éventuellement rajouter deux paramètres très particuliers :
*
args : tuple qui contiendra tous les arguments positionnels supplémentaires (donc autres que ceux déjà prévus) passés à la fonction ;**
kwargs : dictionnaire qui contiendra tous les arguments nommés supplémentaires passés à la fonction. (les clefs du dictionnaire seront les noms des paramètres et les valeurs du dictionnaire seront les valeurs des arguments).
Les noms args et kwargs sont libres et peuvent être remplacés par n'importe quel autre nom de variable valable, mais ceux‑ci correspondent à des conventions entre programmeurs Python.
Les deux sont facultatifs, mais si **
kwargs est présent, il doit alors être placé en dernier. Et si *
args est aussi présent, sous‑entendu qu'il doit toujours être placé avant **
kwargs, il peut alors être placé à n’importe quelle position dans Python 3 (tous les paramètres situés après devront alors être impérativement passés sous forme nominale pour que Python puisse s'y retrouver) et exclusivement en dernière position disponible dans Python 2.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>>
def
etat_civil
(
nom, *
args, grade, **
kwargs):
... print
(
"nom="
, nom, type(
nom))
... print
(
"args="
, args, type(
args))
... print
(
"grade="
, grade, type(
grade))
... print
(
"kwargs="
, kwargs, type(
kwargs))
...
>>>
etat_civil
(
"Cesar"
, "Julius"
, "Caius"
, grade=
"General"
, victoire=
"Alesia"
, defaite=
"Gergovie"
)
nom=
Cesar <
class
'str'
>
args=
(
'Julius'
, 'Caius'
) <
class
'tuple'
>
grade=
General <
class
'str'
>
kwargs=
{'victoire'
: 'Alesia'
, 'defaite'
: 'Gergovie'
} <
class
'dict'
>
Dans cet exemple, le paramètre nom (le premier) reçoit le premier argument passé lors de l’appel ("Cesar"
). Le tuple args reçoit les autres arguments positionnels qui ne sont pas explicitement affectés ("Julius"
et "Caius"
), le paramètre grade reçoit la valeur demandée explicitement par l'appelant ("General"
) et le dictionnaire kwargs reçoit lui les arguments nommés non récupérés par un paramètre adéquat ("Alesia"
et "Gergovie"
) avec les clefs qui sont prises dans les noms d’affectation ("victoire"
et "defaite"
).
IX-7. Utilisation inverse▲
La situation inverse peut survenir lorsque les valeurs à transmettre sont déjà dans une liste, un tuple ou un dictionnaire ; alors que la fonction attend des paramètres bien distincts.
On peut alors envoyer directement la liste ou le tuple vers la fonction en le faisant précéder d’une étoile ; et le dictionnaire en le faisant précéder de deux étoiles. Pour ce dernier, ses clefs doivent correspondre aux noms des paramètres attendus par la fonction.
On nomme cette opération « unpacking ».
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
>>>
def
hypotenuse
(
a, b): return
(
a **
2
+
b **
2
) **
0.5
...
>>>
tpl=(
3
, 4
)
>>>
dico=
{"a"
: 3
, "b"
: 4
} # Clefs identiques aux paramètres
>>>
>>>
hypotenuse
(
tpl[0
], tpl[1
])) # Appel classique
5.0
>>>
hypotenuse
(
dico["a"
], dico["b"
])) # Appel classique
5.0
>>>
>>>
hypotenuse
(*
tpl) # Appel par unpacking du tuple
5.0
>>>
hypotenuse
(**
dico) # Appel par unpacking du dictionnaire
5.0
IX-8. Détails de syntaxe▲
Comme cela a été vu, l’instruction définissant une fonction commence par le mot clef def
suivi de son nom, puis de parenthèses contenant les arguments.
2.
def
add
(
a, b=
0
):
return
a+
b
Pour plus de lisibilité, il est possible de mettre ces arguments sur chaque ligne terminée par une virgule.
2.
3.
4.
5.
def
mult
(
a,
b=
1
,
):
return
a*
b
Spécificité Python 2 : si le dernier argument est *
args ou **
kwargs, ils ne doivent pas se terminer par une virgule. Cette contrainte n’existe plus dans Python 3 qui accepte maintenant les deux syntaxes (avec ou sans virgule).
IX-9. Fonctions anonymes▲
Le mot clef lambda
permet de créer de petites fonctions anonymes.
Exemple :
>>>
hypotenuse=
lambda
a, b: (
a **
2
+
b **
2
) **
0.5
Les fonctions lambda sont syntaxiquement restreintes à une seule expression qui équivaut au return
et peuvent être utilisées partout où un objet fonction est attendu. Elles ne sont qu’un sucre syntaxique remplaçant une définition de fonction à une instruction. Généralement, un usage typique est de passer directement une fonction anonyme en tant qu’argument lorsqu’un outil particulier attend une fonction.
2.
3.
4.
5.
>>>
nbr=
[(
1
, 'un'
), (
2
, 'deux'
), (
3
, 'trois'
), (
4
, 'quatre'
)]
>>>
>>>
# Tableau trié sur chaque x[1], x itérant chaque tuple de nbr, x[1] étant donc la chaîne associée
>>>
sorted
(
nbr, key=
lambda
x: x[1
]) # Tri du tableau – La clef est une fonction lambda
[(
2
, 'deux'
), (
4
, 'quatre'
), (
3
, 'trois'
), (
1
, 'un'
)]
Mais, inversement, il est déconseillé de passer par des fonctions lambda pour s’éviter le travail d’avoir à définir une fonction de façon explicite.
Mauvaise pratique Sélectionnez 1.
|
Bonne pratique Sélectionnez 1.
|
IX-10. Les fonctions incluses▲
Une fonction peut aussi s’inclure dans une autre fonction. Dans ce cas, elle ne sera visible et utilisable que depuis la fonction dans laquelle elle a été incluse.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
>>>
def
a
(
):
... def
xxx
(
):
... print
(
"xxx"
)
... xxx
(
)
... print
(
"a"
)
>>>
a
(
)
xxx
a
>>>
xxx
(
)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
NameError
: name 'xxx'
is
not
defined
Dès lors, deux fonctions incluses dans deux fonctions distinctes peuvent avoir le même nom sans que cela ne gêne.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
>>>
def
a
(
):
... def
xxx
(
):
... print
(
"xxxA"
)
... xxx
(
)
... print
(
"a"
)
>>>
def
b
(
):
... def
xxx
(
):
... print
(
"xxxB"
)
... xxx
(
)
... print
(
"b"
)
>>>
a
(
)
xxxA
a
>>>
b
(
)
xxxB
b
Attention : les fonctions incluses sont recréées à chaque appel de la fonction qui les contient ce qui peut nuire aux performances.
IX-11. La récursivité▲
IX-11-a. Le concept▲
La récursivité est la possibilité offerte à une fonction de s’appeler elle-même ; et à attendre le retour de ce sous-appel pour continuer son propre travail.
L’exemple le plus classique de la récursivité est le calcul de la factorielle d’un nombre « n » (factorielle qui se note « n! ») et qui se calcule de la façon suivante : n!=1*2*3*…*(n-1)*n ; avec la convention que 0!=1.
Algorithmiquement, on se rend vite compte que n!=n*(n-1)! ; ce qui permet de définir la fonction de la façon suivante :
2.
3.
def
factorielle
(
n):
if
n ==
0
: return
1
return
n *
factorielle
(
n-
1
)
Le principe de l’écriture d’une fonction récursive est généralement le suivant :
- écriture du cas entrainant l’arrêt de la récursivité (impératif sinon la fonction n’a aucun repère pour savoir quand arrêter de s’appeler),
- appel de la fonction avec un argument différent de l’argument reçu (impératif sinon le nouvel appel étant identique à l’ancien, la fonction n’a aucun moyen de le différencier).
Et les possibilités syntaxiques de Python permettent de regrouper ces règles de façon plus succinctes.
def
factorielle
(
n): return
n *
factorielle
(
n-
1
) if
n else
1
IX-11-b. Les inconvénients▲
Toutefois, si la récursivité amène une simplicité d’écriture de code, elle entraine en retour une surcharge non négligeable pour le moteur Python qui doit, pour invoquer une fonction récursive, commencer par sauvegarder le contexte actuel avant d’en instancier un nouveau. De plus, la profondeur de récursion est limitée (1000 par défaut).
Cette surcharge devient exponentielle dans le cas de récursivité multiple (la fonction mère appelant plusieurs fois la fonction fille) comme dans le cas de la suite de Fibonacci (U2=U0+U1).
2.
3.
def
fibonacci
(
n):
if
n <
2
: return
(
1
, 1
)[n]
return
fibonacci
(
n-
2
) +
fibonacci
(
n-
1
)
Les appels n’étant pas mémorisés, ceux-ci se font et se refont plusieurs fois inutilement. Exemple : pour U30 il faudra calculer U29 et U28. Pour U29 il faudra calculer U28 (déjà calculé) et calculer U27. Pour U28 (donc calculé 2 fois) il faudra calculer U27 (déjà calculé) et calculer U26. Etc.
C’est pourquoi il est conseillé de réfléchir si un algorithme non récursif (nommé « itératif ») ne serait pas préférable si celui‑ci est possible.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
def
factorielle
(
n):
res=
1
while
n >
1
:
res*=
n
n-=
1
return
res
def
fibonacci
(
n):
res=(
1
, 1
)
if
n <
2
: return
res[n]
while
n >
1
:
res=(
res[1
], res[0
] +
res[1
])
n-=
1
return
res[-
1
]
Si l’algorithme itératif n’est pas possible, il peut-être alors avantageux de simuler la récursivité en utilisant une liste qui servira à empiler et dépiler les appels et résultats.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
def
factorielle
(
n):
res=
1
pile=
[n,]
while
pile:
v=
pile.pop
(
)
if
v >
1
:
res*=
v
pile.append
(
v-
1
)
return
res
def
fibonacci
(
n):
base=(
1
, 1
)
res=
0
pile=
[n,]
while
pile:
v=
pile.pop
(
)
if
v >
1
:
pile.extend
((
v-
1
, v-
2
))
else
:
res+=
base[v]
return
res
Dans ce cas, cela ne change rien aux coûts de calcul (pour le calcul de par exemple U30, U27 sera alors empilé et dépilé 3 fois) mais ça évite les soucis liés à la sauvegarde/restitution du contexte de travail ainsi que la limite de la récursion.
IX-11-c. Optimisation▲
En dehors de sa charge pour le système, un autre inconvénient souvent rencontré dans la mise en place d'une fonction récursive, ce sont les tests préalables et répétés alors de façon récursive.
Reprenons l'exemple de la factorielle en lui faisant vérifier que le paramètre reçu est bien positif :
2.
3.
4.
def
factorielle
(
n):
if
n <
0
: return
0
if
n ==
0
: return
1
return
n *
factorielle
(
n-
1
)
Il est évident que cette vérification n'a de sens que pour le tout premier appel, celui qui est fait par l'utilisateur de la fonction ; et que les appels récursifs effectués par la fonction elle‑même se feront avec un argument correct. Malheureusement ,telle qu'est programmée la fonction, chaque appel récursif effectuera cet inutile contrôle.
Généralement, pour éviter ce souci connu, le développeur programme alors deux fonctions distinctes : la première dédiée aux utilisateurs et se chargeant des contrôles nécessaires, et la seconde dédiée aux calculs et effectuant le travail récursif. Mais cette dernière reste quand même accessible et utilisable par quiconque connait son existence.
Python permet de résoudre cet inconvénient au travers de l'inclusion de fonctions. Le développeur pourra alors encapsuler la fonction récursive dans une couche principale non récursive qui, elle, se chargera des contrôles préalables. Mais la fonction récursive interne ne sera pas accessible depuis l'extérieur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
# Création de la fonction factorielle de façon récursive
def
facto
(
n):
# La sous-fonction moteur du calcul récursif
def
calcul_r
(
n):
if
n ==
0
: return
1
return
n *
calcul_r
(
n-
1
)
# Corps de la fonction principale – Test du paramètre reçu
if
n <
0
: return
0
# Ici tout est ok - Renvoi calcul récursif de la factorielle
return
calcul_r
(
n)