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!
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
780Mais 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: SORTIEet l’utilisation
NOM_DE_FONCTION(VALEUR_CONCRETE_POUR_ENTREE)ATTENTION
le nom utilisée pour l’ENTREE importe peu, si on le change il faut le répercuter dans le SORTIE .
>>> min_en_sec = lambda ms: 60 * ms
>>> min_en_sec(13)
780on peut en fait même réutiliser pour l’ENTREE un nom de variable déjà utilisé, python saura alors faire la différence.
>>> m = 123
>>> min_en_sec = lambda m: 60 * m
>>> min_en_sec(13)
780On peut aussi passer une valeur concrète à la fonction par le biais d’une variable
>>> min_en_sec = lambda m: 60 * m
>>> minutes = 13
>>> min_en_sec(minutes)
780
ou d’une expression qu’il reste à évaluer
>>> min_en_sec = lambda m: 60 * m
>>> minutes = 13
>>> min_en_sec(minutes * 2 - 12)
840En fait on peut même utiliser le même nom pour la variable et l’entrée python fera la connexion uniquement pour la durée de l’appel
>>> m = 23
>>> min_en_sec = lambda m: 60 * m
>>> min_en_sec(13)
780
>>> min_en_sec(m)
1380
>>> min_en_sec(13)
780Finalement notez qu’on peut en fait utiliser plusieurs entrées de la façon suivante
>>> addition = lambda a, b, c: a + b + c
>>> addition(1, 2, 3)
6REMARQUE 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…)
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)
6REMARQUE 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)
3153600000ATTENTION 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)
3Mais 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)
3On 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.0REMARQUE 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)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=150130937545296572356771972164254457814047970568738777235893533016064A 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=2On 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,
... )
31719845REMARQUE
ipython (on utilise la touche de tabulation pour demander la complétion) ou les notebooks. C’est également le cas dans un IDE. On peut de ce fait découvrir comment utiliser une fonction rien que de cette façon.On peut aussi passer des valeurs par défaut qui permettent de ne rentrer que les arguments utiles. En adaptant l’exemple précédent:
>>> def conversion_duree(
... annees=0,
... jours=0,
... heures=0,
... minutes=0,
... secondes=0
... ):
... j = jours + 365 * annees
... h = heures + 24 * j
... m = minutes + 60 * h
... s = secondes + 60 * m
... return s
...
>>> conversion_duree(minutes=45)
2700
>>> conversion_duree(heures=2, annees=1)
31543200ont_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.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.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.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.>>> help("lambda")>>> help("FUNCTIONS")>>> help("def")>>> help("return")>>> help("CALLS")>>> 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>>> 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>>> 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.2Cette 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.2Mais 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')]