IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

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

 
Sélectionnez
1.
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

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
>>> 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…

 
Sélectionnez
1.
2.
3.
4.
>>> fct(1, b=2)
1 2
>>> fct(a=1, b=2)
1 2

… mais l’appel suivant est incorrect :

 
Sélectionnez
1.
>>> 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 :

 
Sélectionnez
1.
>>> 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…

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.
def divisible(a, b=1):
    return (a % b) == 0
Nouvelle syntaxe possible
Sélectionnez
1.
2.
def divisible(a:int, b:int=1) ‑> bool:
    return (a % b) == 0

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.

 
Sélectionnez
1.
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 ».

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
>>> 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.

 
Sélectionnez
1.
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.
lambda sqrt n: n ** 0.5
Bonne pratique
Sélectionnez
1.
def sqrt(n): return n ** 0.5

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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)

précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.