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
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
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)
780
on 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)
780
On 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)
840
En 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)
780
Finalement 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)
6
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…)
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)
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
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)
31543200
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.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.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')]