Créer ses fonctions

Une fonction est un bloc de code exécutable. On peut lui associer un nom et exécuter ce code juste en l'appelant : ceci permet de ne pas copier/coller des lignes code identiques à différents endroit du programme.

Il n'est jamais bon de copier/coller un bout de programme qui se répète plusieurs fois (corriger un problème dans ce bout de code reviendrait à le corriger autant de fois qu'il a été dupliqué... si on se rappelle des endroits où il l'a été). Il est de plus souvent utile de séparer les éléments logiques d'un programme en unités autonomes, ceci rend le programme plus facile à relire.

Définition d'une fonction

Une fonction est un bloc auquel on donne un nom (le nom de la fonction) qui peut être exécuté lorsqu'on l'invoque par son nom.

def <nom>(paramètre 1, paramètre 2, ..., paramètre n):
    instruction 1
    instruction 2
    ...
    instruction n
    return <objet>

Les paramètres et la dernière la dernière ligne avec return sont optionnelles.

La partie de programme suivant définit une fonction :

def bonjour():
    print("Salutations")

La première ligne est la définition du bloc fonction. Il contient :

Ensuite vient le bloc fonction en lui-même qui ne contient ici qu'une seule ligne.

Si on exécute le bloc précédent, il ne se passe rien. En effet on n'a fait que définir la fonction. Pour l'utiliser, ajoutez bonjour() à la suite du bloc.

Une fonction s'utilise toujours en faisant suivre son nom d'une parenthèse contenant ses paramètres séparés par une virgule (notre fonction n'a pour l'instant pas de paramètres). Donner juste son nom ne suffit pas à l'invoquer.

Nom d'une fonction

Un nom de fonction est variable comme une autre. Elle est affectée dans l'espace de nom du bloc dans lequel elle est défini.

Dans le code suivant, exécuté dans un interpréteur on regarde le type d'un nom associé à une fonction :

>>> def bonjour():
...     print("Salutations")
... 
>>> type(bonjour)
<class 'function'>

On peut aussi associer la fonction à une autre variable comme on le ferait avec n'importe quel autre objet. Dans l'exemple suivant on associe la fonction à une autre variable, x :

>>> def bonjour():
...     print("Salutations")
... 
>>> x = bonjour
>>> x()
Salutations

En python, lorsque l'on exécute une fonction on dit qu'on l'appelle. Appeler une variable est alors le fait de mettre des () après son nom.

Si cela produit une erreur ce n'était pas une fonction. Regardez l'exemple ci-après, exécutable dans un interpréteur. On tente d'appeler un entier et python nous indique que ce n'est pas possible :

>>> n = 3
>>> n()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

Enfin, en python être une fonction n'est rien d'autre que d'être un objet appelable. Savoir si un objet est appelable ou pas se fait par la fonction callable :

>>> def bonjour():
...     print("Salutations")
... 
>>> callable(bonjour)
True
>>> callable(1)
False
>>> callable("ee")
False

Les fonctions ne sont pas les seules objets appelables, les types le sont également : le résultat de l'appel du type list (c'est à dire list()) crée une liste vide.

Il en existe de nombreux autres, python étant friand de ce genre d'opérations.

Paramètres d'une fonction

def plus_moins(nombre):
    if nombre > 42:
        print("Supérieur à 42")
    else:
        print("Inférieur à 42")

Cette fonction nécessite donc un paramètre pour être invoquée. Testez alors plus_moins(17).
La variable nombre sera associée à l'objet entier de valeur 17 dans la fonction. La variable nombre n'existe que dans la fonction.

Python, à chaque exécution d'une fonction crée un espace de nom pour elle. Cet espace de nom sera détruit lorsque la fonction aura fini d'être exécutée. Une fois cet espace de nom crée, il associe le nom du paramètre à l'objet passé en paramètre.

Les paramètres d'une fonction sont des noms de variables qui ne seront connus qu'à l'intérieur de la fonction. À l'exécution de la fonction, le nom de chaque paramètre est associé à l'objet correspondant.

Regardons le bout de code suivant, qui utilise la fonction plus_moins définie précédemment :

x = 12
plus_moins(x)

Lorsque python exécute la deuxième du code précédent il va :

  1. créer un espace de nom pour la fonction
  2. regarder les objets passés en paramètre. Ici c'est l'objet associé au nom x. Python cherche l'objet, c'est un entier valant 12.
  3. python associe chaque objet à son nom dans l'espace de nom de la fonction : ici l'entier qui vaut 12 sera appelé nombre dans la fonction (le nom du paramètre dans la définition de la fonction).
  4. python exécute la fonction.
  5. à la fin de la fonction, l'espace de nom de la fonction est détruit (on ne détruit que les noms, pas les objets associés).

Créez et testez une fonction nommée cube qui prend un entier en paramètre et affiche cet élément au cube.

solution

def cube(x):
    print(x ** 3)

cube(2)

Créez et testez une fonction nommée puissance qui prend deux entiers en paramètre et affiche à l'écran le premier paramètre élevé à la puissance du second paramètre.

solution

def puissance(x, y):
    print(x ** y)

puissance(2, 3)
puissance(3, 2)

Paramètres par défaut

def plus_moins(nombre, seuil=42):
    if nombre > seuil:
        print("Supérieur à", seuil)
    else:
        print("Inférieur à", seuil)

On peut alors utiliser la fonction comme précédemment, plus_moins(20), ou en utilisant le paramètre seuil plus_moins(20, seuil=10).

Comme le paramètre par défaut est le deuxième on peut aussi l'utiliser sans le nommer : plus_moins(20, 10)

Créez et testez une fonction nommée puissance qui prend deux entiers en paramètre et affiche le premier paramètre élevé à la puissance du second paramètre. Le second paramètre vaut 2 par défaut.

solution

def puissance(x, y=2):
    print(x ** y)

Retour d'une fonction

Toute fonction peut rendre une valeur. On utilise le mot-clef return suivi de la valeur à rendre pour cela. Le fonction suivante rend le double de la valeur de l'objet passé en paramètre:

def double(valeur):
    x = valeur * 2
    return x

Il ne sert à rien de mettre des instructions après une instruction return car dès qu'une fonction exécute cette instruction, elle s'arrête en rendant l'objet en paramètre. Le retour d'une fonction est pratique pour calculer des choses et peut ainsi être affecté à une variable.

Dans un notebook ou dans Spyder, définissez la fonction précédente dans une cellule puis exécutez là.

Puis, dans une seconde cellules collez la ligne ci-après puis exécutez la.

print(double(21))

Le résultat de la cellule devrait être : 42.

Le code précédent exécute la fonction de nom double avec comme paramètre un entier de valeur 21. La fonction commence par associer à une variable nommée valeur l'objet passé en paramètre (ici un entier de valeur 21), puis crée une variable de nom x à laquelle est associée un entier de valeur 42 et enfin se termine en retournant comme valeur l'objet de nom x. Les variables valeur et x définies à l'intérieur de la fonction sont ensuite effacées (pas les objets, seulement les noms).

Cette valeur retournée est utilisée par la commande print pour être affichée à l'écran.

Les noms de paramètres d'une fonction et les variables déclarée à l'intérieur de la fonction n'existent qu'à l'intérieur de celle-ci. En dehors de ce blocs, ces variables n'existent plus.

Créez et testez une fonction nommée puissance qui prend deux entiers en paramètre et rend le premier paramètre élevé à la puissance du second paramètre. Le second paramètre vaut 2 par défaut.

solution

def puissance(x, y=2):
    return x ** y

Fonction en paramètre

Une fonction étant un objet comme un autre, elle peut très bien être utilisée comme paramètre :

def calcul(fct, z):
    return fct(2, 17) + z

Le premier paramètre de la fonction calcul est appelé avec deux paramètres et son résultat est additionné au second paramètre.

La ligne suivante est alors du python correct :

print(calcul(produit, 8))

Si on a au préalable définit produit comme une fonction à deux paramètres, comme par exemple :

def produit(x, y):
    return x * y

Exécutez le code précédent et expliquer son fonctionnement

solution

Le code final doit définir produit avant son utilisation. Il faut par exemple avoir le code :

def calcul(fct, z):
    return fct(2, 17) + z

def produit(x, y):
    return x * y

print(calcul(produit, 8))

Notez que lors de la définition de la fonction calcul, la variable fct n'est qu'un paramètre anonyme. Ce paramètre ne doit être défini que lors de son appel, à la ligne 7.

La ligne 7 fonctionne alors comme suit :

  1. l'objet de type fonction de nom produit est passé en paramètre de la fonction calcul
  2. le retour de l'appel calcul(produit, 8) est égal à $8 + (2 * 17) = 42$ puisque fct est la fonction produit.
  3. son retour (42) est ensuite affiché à l'écran grâce à la fonction print

Lambda

Les lambda sont ue façon d'écrire rapidement une fonction avec une unique instruction.

Les deux codes suivant sont identiques :

double = lambda x: 2 * x

et :

def double(x):
    return 2 * x

Le principal intérêt de ces fonction est d'être utilisée comme paramètre d'autres fonction.

Par exemple avec le paramètre key de la méthode de liste sort. Considérons la liste l :

l = [["au revoir", 2], ["bonjour", 1]]

Si on cherche à trier l, la liste sera triée en comparant le 1er élément de chaque liste :

l.sort()

print(l)  # donnera [['au revoir', 2], ['bonjour', 1]]

Si l'on veut trier sur le deuxième élément de chaque liste, on utilise le paramètre key qui est une fonction. Les éléments $x$ de la liste seront triés selon $key(x)$ plutôt que $x$ :

def second(x):
    return x[1]

l.sort(key=second)

print(l)  # donnera [['bonjour', 1], ['au revoir', 2]]

Que donnerait le tri si la fonction second avait été définie comme ceci :

def second(x):
    return 1 / x[1]

solution

def second(x):
    return 1 / x[1]

l = [["au revoir", 2], ["bonjour", 1]]

l.sort(key=second)

print(l)

Utiliser une fonction lambda permet de raccourcir le code précédent tout en le gardant très clair :

l = [["au revoir", 2], ["bonjour", 1]]

l.sort(key=lambda x: x[1])

print(l)  # donnera [['bonjour', 1], ['au revoir', 2]]

Annotations de type

Les annotations de types permettent de renseigner le type des entrées et de la sortie d'une fonction python. Il n'est pas nécessaire de le faire, mais si vous avez besoin d'expliciter une signature de fonction comme on le ferait dans un langage compilé comme java, vous pouvez le faire en ajoutant :

Par exemple, la fonction suivante permet de savoir si un élément est dans une liste :

def recherche(t, x):
    for e in t:
        if e == x:
            return True
    return False

Si l'on veut restreindre cette fonctions aux listes d'entier on pourra écrire :

def recherche(t: [int], x: int) -> bool
    for e in t:
        if e == x:
            return True
    return False

La plupart du temps, pour de petits programme, ce genre de précision n'est pas importante. Elle ne devient cruciale que lorsque la base de code grossit et que spécifier les types d'entrée évite les bug.

Mais alors, il est de toute façon plus pertinent d'écrire dans un autre langage que python... Plus adapté au développement de grosses applications comme le java ou encore le rust.