Fonctions: Acte I

Motivation

Les mécanismes que l’on a vu jusqu’à présent permettraient théoriquement de réaliser la majorité des tâches courantes (même si on n’a pas encore vu la gestion des erreurs et les interactions avec les fichiers).

La difficulté principale pour les tâches ambitieuses est de gérer la complexité d’un projet. Ainsi on peut imaginer la difficulté à écrire, à comprendre et à garantir le caractère correct d’un script de quelques milliers d’instructions successives.

On va voir ici le premier des trois grands mécanismes classiques utilisés pour gérer la complexité: les fonctions!

Exemple

Quand on avait voulu estimer le nombre de secondes dans un siècle, on avait introduit des variables intermédiaires. Ceci afin que les formules soient plus lisibles. Typiquement

>>> secondes_par_minutes = 60
>>> minutes = 13
>>> secondes = secondes_par_minutes * minutes
>>> secondes
780

Mais on peut en fait simplifier encore ceci en créant une “variable à paramètre” (en l’occurrence une fonction)

>>> min_en_sec = lambda m: 60 * m
>>> min_en_sec(13)
780
>>> min_en_sec(23)
1380
>>> type(min_en_sec)
<class 'function'>

Ici lambda est un mot réservé par python; min_en_sec fait référence à un objet qui lorsqu’on lui fournit m nous renvoie l’évaluation de l’expression 60 * m.

Notez qu’on a ainsi créé une recette qu’on peut réutiliser autant de fois qu’on le souhaite (2 fois au dessus) ce qui fait que l’on a un seul endroit à modifier pour adapter la fonctionnalité ou corriger un bug. On a aussi pu nommer la tâche effectuée pour encore plus de lisibilité.

On voit que l’utilisation (plus précisément l’appel) de la fonction est en fait le nom de la fonction suivi de la valeur que l’on souhaite utiliser pour l’entrée, entourée de parenthèses.

La définition se fait ainsi de cette façon

NOM_DE_FONCTION = lambda ENTREE: SORTIE

et l’utilisation

NOM_DE_FONCTION(VALEUR_CONCRETE_POUR_ENTREE)

ATTENTION

REMARQUE l’utilisation du mot clé lambda vient du λ-calculus d’Alonzo Church une théorie de la calculabilité datant des années 30 (avant les ordinateurs et même la notion de machine de Turing donc) et qui a inspiré le paradigme de programmation fonctionnelle et de nombreux langages (lisp, haskell…)

Définition d’une fonction

De fait la syntaxe précédente est peu utilisée en python car elle devient impraticable dès que la “recette” devient compliquée et nécessite des étapes intermédiaires.

Elle n’est même quasiment utilisée que pour créer des fonctions anonymes que l’on n’utilise qu’une fois, typiquement comme argument d’autres fonctions (Regarder par exemple la documentation de la fonction sorted, plus précisément l’argument key).

A la place, python fournit une instruction composée def pour la définition, la partie appel est par contre inchangée.

Comme premier exemple donnons la nouvelle version des fonctions que l’on a vu au dessus

>>> def min_en_sec(m):
...     return 60 * m
...
>>> min_en_sec(13)
780
>>> def addition(a, b, c):
...     return a + b + c
...
>>> addition(1, 2, 3)
6

REMARQUE notez bien l’utilisation de l’instruction return à l’intérieur du corps de l’instruction def pour signaler ce qui doit être renvoyé.

On peut donc maintenant rajouter des étapes intermédiaires avant de retourner

>>> def annees_en_secondes(ans):
...     jours = ans * 365
...     heures = 24 * jours
...     minutes = 60 * heures
...     secondes = 60 * minutes
...     return secondes
...
>>> annees_en_secondes(100)
3153600000

ATTENTION une erreur classique consiste à confondre print et return. En effet dans un premier temps les deux peuvent se ressembler:

>>> def ajoute(a, b):
...     print(a + b)
...
>>> ajoute(1, 2)
3
>>> def ajoute(a, b):
...     return a + b
...
>>> ajoute(1, 2)
3

Mais dès que l’on essaye d’utiliser autrement le retour on voit apparaître la différence.

>>> def ajoute(a, b):
...     print(a + b)
...
>>> resultat = ajoute(1, 2)
3
>>> resultat
>>> type(resultat)
<class 'NoneType'>
>>> def ajoute(a, b):
...     return a + b
...
>>> resultat = ajoute(1, 2)
>>> resultat
3
>>> type(resultat)
<class 'int'>

print est ce que l’on appelle un effet de bord, l’utilisateur de la fonction ne peut rien faire d’autre que visualiser le résultat. Cela va totalement à l’encontre de l’objectif des fonctions qui est de décomposer une tâche complexe. On évitera donc systématiquement l’utilisation de print sauf pour analyser le comportement d’une fonction (et donc de manière transitoire).

REMARQUE les effets de bords consistent en toutes les interactions de la fonctions avec le monde extérieur qui ne passent pas par les arguments ou le retour. On verra dans la prochaine leçon d’autres exemples et pourquoi les éviter.

REMARQUE dès que l’on tombe sur l’instruction return on sort de la fonction et ce qui suit n’est donc pas exécuté.

>>> def exemple(a, b):
...     resultat = a + b
...     return resultat
...     print("AHAH!")
...     resultat = 10
...
>>> exemple(1, 2)
3

On verra que cette propriété est utilisée lors d’instruction conditionnelles.

>>> def division(a, b):
...     if b == 0:
...             return "PROBLEME"
...     return a / b
...
>>> division(123, 0)
'PROBLEME'
>>> division(12, 1)
12.0

REMARQUE Si on finit le corps principal de la fonction sans rencontrer d’instruction return, python insère automatiquement une ligne return None (c’était le cas dans l’exemple avec print).

None est un objet python représentant le néant (l’absence d’objet…) Ce sera une source de bogues lorsqu’on a une logique interne trop compliquée et des return à la fin d’instructions conditionnelles non exhaustives. Voici un exemple trop simple pour être réaliste mais qui donne une idée du problème.

>>> def compliquee(a, b):
...     if a == b:
...             return a
...     elif a < b:
...             return b
...
>>> compliquee(1, 1)
1
>>> compliquee(1, 2)
2
>>> compliquee(2, 1)

REMARQUE si l’on souhaite renvoyer plusieurs valeurs on utilisera un tuple (et après return on peut de fait se passer des parenthèses)

>>> def multiple_retour(a, b):
...     if b == 0:
...             return (float("nan"), float("nan"))
...     return a // b, a % b
...
>>> multiple_retour(123, 11)
(11, 2)
>>> multiple_retour(12, 0)
(nan, nan)

Utilisation d’une fonction: appel

Comme on l’a vu l’appel d’une fonction consiste en son nom puis entre parenthèses des valeurs concrètes (éventuellement sous forme d’expressions). Les instructions du corps principal sont alors exécutée avec les noms des arguments remplacés par les valeurs passées.

>>> def visualisation(a, b, c):
...     print(f"a={a}")
...     print(f"b={b}")
...     print(f"c={c}")
...
>>> visualisation(1, 2, 3)
a=1
b=2
c=3
>>> visualisation(1 + 1, 2 * 2, 3 ** 3)
a=2
b=4
c=27
>>> variable = 42
>>> visualisation(variable, 2 * variable, variable ** variable)
a=42
b=84
c=150130937545296572356771972164254457814047970568738777235893533016064

A ce stade on voit que la première valeur remplace le premier argument et ainsi de suite. Python possède un mécanisme pour ne pas prendre en compte l’ordre au moment de l’appel.

>>> def visualisation(a, b, c):
...     print(f"a={a}")
...     print(f"b={b}")
...     print(f"c={c}")
...
>>> visualisation(1, 2, 3)
a=1
b=2
c=3
>>> visualisation(b=1, c=2, a=3)
a=3
b=1
c=2

On précède ainsi la valeur du nom, les deux étant séparés par un égal. On parle alors de passage d’arguments nommés. On peut théoriquement mélanger l’utilisation d’arguments ordonnés et nommés mais on se référera à la documentation pour les règles précises. De fait on utilisera principalement l’un ou l’autre.

REMARQUE si les noms des arguments sont bien choisis, l’utilisation d’arguments nommés peut nettement améliorer la lisibilité. L’exemple suivant montre à la fois ceci et présente aussi le formatage à utilisée lorsqu’une ligne devient trop grande.

>>> def conversion_duree(
...     annees,
...     jours,
...     heures,
...     minutes,
...     secondes
... ):
...     j = jours + 365 * annees
...     h = heures + 24 * j
...     m = minutes + 60 * h
...     s = secondes + 60 * m
...     return s
...
>>> conversion_duree(
...     annees=1,
...     jours=2,
...     heures=3,
...     minutes=4,
...     secondes=5,
... )
31719845

REMARQUE

Exercices

  1. Construire une fonction ont_meme_parite prenant en entrée a: int, b: int et c: int et renvoyant True s’ils ont tous la même parité et False sinon.
  2. Coder une fonction compte_occurrences prenant en entrée lettre: str et message: str, et renvoyant le nombre de fois où la lettre apparaît dans le message.
  3. Coder une fonction calcule_moyenne prenant en entrée valeurs: list[int] et renvoyant la moyenne des valeurs une fois retirée la plus grande et la plus petite.
  4. Coder une fonction filtre prenant en entrée couples: list[tuple[int, str]] et qui renvoie la liste constituée exactement des éléments de couples dont la chaine de caractères en deuxième élément à pour longueur l’entier en première position.

Pour aller plus loin

>>> help("lambda")
>>> help("FUNCTIONS")
>>> help("def")
>>> help("return")
>>> help("CALLS")

Corrections

  1. >>> def ont_meme_parite(a, b, c):
    ...     if (a - b) % 2 != 0:
    ...             return False
    ...     if (a - c) % 2 != 0:
    ...             return False
    ...     return True
    ...
    >>> ont_meme_parite(1, 3, 5)
    True
    >>> ont_meme_parite(1, 2, 3)
    False
    >>> ont_meme_parite(2, 4, 6)
    True
  2. >>> def compte_occurrences(lettre, message):
    ...     nombre_occurrences = 0
    ...     for caractere in message:
    ...             if caractere == lettre:
    ...                     nombre_occurrences = nombre_occurrences + 1
    ...     return nombre_occurrences
    ...
    >>> compte_occurrences("a", "ababa")
    3
    >>> compte_occurrences("e", "ababa")
    0
  3. >>> def calcule_moyenne(valeurs):
    ...     somme, nombre = 0, 0
    ...     m, M = valeurs[0], valeurs[0]
    ...     for valeur in valeurs:
    ...             somme = somme + valeur
    ...             nombre = nombre + 1
    ...             if m > valeur:
    ...                     m = valeur
    ...             if M < valeur:
    ...                     M = valeur
    ...     resultat = (somme - M - m) / (nombre - 2)
    ...     return resultat
    ...
    >>> calcule_moyenne([1, 2, 5])
    2.0
    >>> calcule_moyenne([1, 2, 3, 4, 3, 2, 1])
    2.2

    Cette version est en fait simplifiable de la façon suivante:

    >>> def calcule_moyenne(valeurs):
    ...     somme = sum(valeurs)
    ...     nombre = len(valeurs)
    ...     m = min(valeurs)
    ...     M = max(valeurs)
    ...     resultat = (somme - M - m) / (nombre - 2)
    ...     return resultat
    ...
    >>> calcule_moyenne([1, 2, 5])
    2.0
    >>> calcule_moyenne([1, 2, 3, 4, 3, 2, 1])
    2.2

    Mais bien sûr la première version est plus généralisable.

ATTENTION en fait la fonction devrait être adaptée pour traiter le cas des listes à moins de 3 éléments. 4.

>>> def filtre(couples):
...     resultat = list()
...     for nombre, chaine in couples:
...             if nombre == len(chaine):
...                     resultat.append((nombre, chaine))
...     return resultat
...
>>> entree = [(1, "a"), (2, "abc"), (-4, 'abcd'), (4, "abcd")]
>>> filtre(entree)
[(1, 'a'), (4, 'abcd')]