Skip to content

Les fonctions

Rappel d'ouverture (5 minutes, cours fermé)

Réponds sans rouvrir les pages précédentes, en écrivant tes réponses.

  1. Quelle différence entre écrire elif et écrire un second if juste après le premier ?
  2. Écris la boucle qui compte les "a" de la chaîne mot. Où se place l'initialisation du compteur ?
  3. Que vaut len("bonjour") et que vaut "bonjour"[3] ?
Corrigé
  1. Un elif n'est testé que si la condition précédente est fausse : les branches s'excluent. Deux if successifs sont testés tous les deux, et leurs blocs peuvent s'exécuter l'un après l'autre.
  2. L'initialisation est avant la boucle, la mise à jour dedans :
    compteur = 0
    for c in mot:
        if c == "a":
            compteur = compteur + 1
    
  3. 7 et "j".

Pourquoi des fonctions ?

En programmation, on a souvent besoin de réutiliser du code, à quelque chose près.

Imagine qu'on doive dessiner un carré et que cela prenne 10 lignes de code. Pour dessiner 50 carrés, faut-il écrire 500 lignes ? Non. On range le code qui dessine un carré dans une fonction, et il suffit ensuite d'une ligne pour l'appeler.

Tu utilises déjà des fonctions écrites par d'autres. print appelle elle-même tout un tas de fonctions, jusque dans les tréfonds de l'ordinateur, pour allumer les bons pixels à l'écran.

Définition : fonction

Une fonction est un bloc de code nommé et réutilisable qui réalise une tâche précise.

Une fonction a :

  • un nom ;
  • des paramètres ;
  • du code à exécuter ;
  • et elle peut éventuellement renvoyer une donnée.

1. Avant tout : la signature

La méthode, dans l'ordre

Avant d'écrire la moindre ligne de code, on définit le contrat de la fonction :

  1. sa signature ;
  2. sa docstring (ce qu'elle fait) ;
  3. puis seulement son code ;
  4. et son return si elle renvoie quelque chose.

Ce que l'IA ne change pas

Une IA écrit le corps d'une fonction en une seconde. Elle sait aussi écrire la signature, la docstring et les tests : ne compte pas sur une incapacité de sa part, il n'y en a pas ici.

Ce qui change, c'est ta position. Pour accepter ou refuser ce qu'elle te propose, il faut que tu saches énoncer le contrat toi-même : que prend la fonction, que renvoie-t-elle, dans quels cas. Sans ce contrat en tête, tu ne peux pas juger le code produit, tu ne peux que le croire. Et tu es évalué sans elle.

Commençons par lire une fonction qui existe déjà, randint, avant d'en écrire une nous-mêmes.

from random import randint

valeur_de_retour = randint(3, 50)
print(valeur_de_retour)
help(randint)

La tâche de randint est de renvoyer un entier pseudo-aléatoire dans un intervalle \([a..b]\).

  • elle a un nom : randint (random integer) ;
  • elle a 2 paramètres : la borne basse, puis la borne haute ;
  • elle exécute du code (rien ne tombe du ciel) ;
  • elle renvoie une donnée : le nombre tiré.

Voici comment sa première ligne est écrite :

def randint(a: int, b: int) -> int:
    """Renvoie un entier dans l'intervalle [a, b]"""
    ...  # le code

Définition : la signature

La première ligne d'une fonction est sa signature. Elle comprend :

  1. le nom de la fonction ;
  2. la liste des paramètres, entre parenthèses, chacun avec son nom et son type (nom: type) ;
  3. le type de la valeur renvoyée, après une flèche -> (facultatif mais recommandé).

La ligne juste en dessous est la docstring : une phrase qui décrit ce que fait la fonction. Le code vient ensuite, indenté.

Savoir-faire : écrire une signature

def randint(a: int, b: int) -> int:
  1. le mot-clé def annonce la signature ;
  2. un nom évocateur de la tâche (randint) ;
  3. les paramètres entre parenthèses, séparés par des virgules, chacun sous la forme nom: type ;
  4. une flèche -> suivie du type de la donnée renvoyée ;
  5. on n'oublie pas les deux-points à la fin.

Cette ligne dit : la fonction s'appelle randint, prend deux entiers a et b, et renvoie un entier.

Prendre du recul : la même idée dans d'autres langages

Signature d'une fonction addition de deux entiers, selon le langage :

fn addition(a: i32, b: i32) -> i32          // Rust
public static int addition(int a, int b)    // Java, C#
int addition(int a, int b)                  // C, C++
function addition(a: number, b: number): number   // TypeScript
def addition(a, b)                          # Ruby (types non déclarés)

Partout, on retrouve un nom, des paramètres et (souvent) un type de retour.

Exercice papier : écrire des signatures et des docstrings

Pour chaque fonction, trouve un nom et des noms de paramètres adéquats, et renseigne bien les types des paramètres et du retour. N'écris pas encore le code, seulement la signature et la docstring.

  1. Calcule le cube d'un nombre entier.
  2. Vérifie si un nombre entier est pair.
  3. Concatène deux chaînes de caractères.
  4. Calcule la somme des éléments d'une liste d'entiers.
  5. Vérifie si un élément est présent dans une liste d'entiers.
  6. Convertit une température de degrés Celsius en degrés Fahrenheit.
  7. Trouve le plus grand élément d'une liste d'entiers.
  8. Calcule la moyenne des notes d'une liste de notes.
  9. Vérifie si un texte est un palindrome.

Aide sur les types, dont un que tu n'as pas encore vu

Une liste de quelque chose est de type list. Une liste d'entiers se note list[int], une liste de chaînes list[str], une liste de listes d'entiers list[list[int]].

Les listes sont étudiées plus loin, dans Les listes : ici, tu n'as pas à savoir les manipuler, seulement à écrire le type dans la signature. C'est justement l'intérêt de l'exercice : on peut énoncer un contrat sur une donnée dont on ne sait pas encore programmer le traitement.

2. Le corps de la fonction et return

Pour coder une fonction, on part de sa signature et du nom de ses paramètres. On considère que la fonction ne connaît que ses paramètres, pas les variables extérieures.

On écrit du code normalement, et on utilise le mot-clé return pour renvoyer le résultat calculé.

def carre(x: float) -> float:
    """Calcule le carré du flottant passé en paramètre"""
    resultat = x * x
    return resultat

Quand on exécute cette cellule, il ne se passe rien à l'écran : on a seulement appris à Python à calculer un carré. Pour qu'il le fasse, il faut appeler la fonction :

var = carre(7)
print(var)     # affiche 49

Ce qui se passe vraiment lors d'un appel

Dérouler b = carre(5.3) pas à pas

  1. Python rencontre l'appel carre(5.3). Il met la suite de côté et entre dans la fonction.
  2. L'argument 5.3 est rangé dans le paramètre x : à l'intérieur, x vaut 5.3.
  3. Le corps s'exécute : resultat = x * x, donc resultat vaut 28.09.
  4. return resultat : la fonction renvoie 28.09 et s'arrête aussitôt.
  5. La valeur renvoyée remplace l'appel : la ligne devient b = 28.09.
  6. b vaut donc 28.09.

À retenir : un appel de fonction se comporte comme la valeur qu'il renvoie. carre(5.3) finit par « valoir » 28.09, et peut donc s'utiliser partout où une valeur est attendue.

return en bref

  • Dès que Python rencontre un return, il quitte la fonction.
  • Sans return, une fonction ne renvoie rien d'utilisable.
  • Il peut y avoir plusieurs return, mais quand on peut l'éviter, c'est plus clair.

Piège classique : return n'est pas print

Ces deux fonctions se ressemblent, mais ne font pas la même chose :

def carre_ok(x: float) -> float:
    return x * x

def carre_faux(x: float) -> float:   # ERREUR fréquente à ne pas reproduire
    print(x * x)
  • carre_ok(5) renvoie 25 : on peut réutiliser cette valeur, b = carre_ok(5) met 25 dans b.
  • carre_faux(5) affiche 25 mais ne renvoie rien : b = carre_faux(5) met None dans b.

print sert à montrer quelque chose à l'utilisateur. return sert à rendre une valeur au reste du programme. D'ailleurs, print elle-même renvoie None :

x = print("bonjour")   # affiche : bonjour
print(x)               # affiche : None

Le résultat d'un appel est une valeur comme une autre : on peut l'utiliser dans des calculs.

Prédire avant d'exécuter

Sans lancer le code, prédis ce qui s'affiche, puis vérifie :

a = 5.3
b = carre(a)
print(b)
print(carre(a + b))
print(carre((6 * b - carre(a)) / (5 + carre(b))))

(Attention aux parenthèses : un appel de fonction est une valeur, on peut donc l'imbriquer.)

Périmètre

  1. Écris une fonction qui calcule le périmètre d'un rectangle.
  2. Calcule le périmètre d'un rectangle de dimensions \(14 \times 50\).
Corrigé
def perimetre(largeur: float, hauteur: float) -> float:
    """Renvoie le périmètre d'un rectangle de dimensions largeur x hauteur"""
    return 2 * (largeur + hauteur)

print(perimetre(14, 50))   # 128

XOR

Le « ou exclusif » (xor) est vrai quand ses deux entrées sont différentes :

c1 c2 c1 xor c2
Vrai Vrai Faux
Vrai Faux Vrai
Faux Vrai Vrai
Faux Faux Faux

Écris une fonction xor qui prend deux booléens et renvoie True ou False selon les cas. Teste-la dans les quatre cas.

Corrigé
def xor(c1: bool, c2: bool) -> bool:
    """Renvoie le ou exclusif des deux booléens c1 et c2"""
    return c1 != c2

3. Fonctions sans valeur de retour

Une fonction peut ne renvoyer aucune valeur : son rôle est alors d'effectuer un travail (afficher, dessiner...) sans rendre d'information. C'est le cas de print.

def dis_coucou(n: int) -> None:
    """Affiche coucou n fois"""
    for _ in range(n):
        print("Coucou!")

dis_coucou(3)
a = dis_coucou(2)
print(a)      # affiche None

Quand une fonction ne renvoie rien, on peut omettre le return. Et une fonction qui ne renvoie « rien » renvoie tout de même quelque chose : None, qui signifie « aucune valeur ».

4. Fonctions sans paramètre

Une fonction peut n'avoir aucun paramètre (comme input()). Il faut alors tout de même écrire les parenthèses vides en signature, pour indiquer qu'il s'agit d'une fonction et non d'une variable.

def demander_prenom() -> str:
    """Demande son prénom à l'utilisateur et le renvoie"""
    return input("Ton prénom ? ")

5. Fonctions à plusieurs paramètres

Une fonction peut avoir autant de paramètres qu'on veut. À l'appel, il faut respecter leur nombre et leur ordre.

Rectangle de caractères

L'appel affiche_rectangle(2, 5, 'A') doit afficher :

AAAAA
AAAAA
Écris et teste affiche_rectangle. Rappels : print() passe à la ligne ; print('A', end='') affiche sans passer à la ligne.

Indice léger

Un rectangle, ce sont des lignes ; une ligne, ce sont des caractères. Deux choses à répéter, donc deux boucles, et l'une est à l'intérieur de l'autre. Laquelle dedans ?

Indice plus précis

La boucle extérieure compte les lignes (hauteur tours). La boucle intérieure affiche les caractères d'une ligne (largeur tours), avec end='' pour rester sur la même ligne. Le retour à la ligne se fait avec un print() vide, placé dans la boucle extérieure mais après la boucle intérieure.

Corrigé
def affiche_rectangle(hauteur: int, largeur: int, car: str) -> None:
    """Affiche un rectangle de hauteur x largeur fait du caractère car"""
    for _ in range(hauteur):
        for _ in range(largeur):
            print(car, end='')
        print()

Automatismes

Écris la signature, la docstring puis le code de :

  1. nb_secondes(heures, minutes, secondes) : renvoie le nombre total de secondes.
  2. conversion_euro_dollar(euros) : convertit des euros en dollars (taux \(1€ = 1{,}17\$\)). Convertis 12 €.
  3. pair_ou_impair(n) : renvoie "pair" ou "impair".
  4. plus_petit(a, b) : renvoie le plus petit des deux entiers.
Corrigé
def nb_secondes(heures: int, minutes: int, secondes: int) -> int:
    """Renvoie le nombre total de secondes"""
    return heures * 3600 + minutes * 60 + secondes

def conversion_euro_dollar(euros: float) -> float:
    """Convertit un montant en euros vers des dollars"""
    return euros * 1.17

def pair_ou_impair(n: int) -> str:
    """Renvoie 'pair' ou 'impair' selon la parité de n"""
    if n % 2 == 0:
        return "pair"
    return "impair"

def plus_petit(a: int, b: int) -> int:
    """Renvoie le plus petit des deux entiers"""
    if a < b:
        return a
    return b

IMC

  1. Écris imc(poids, taille) qui renvoie l'indice de masse corporelle (formule sur Wikipédia).
  2. Écris un programme qui demande poids et taille, puis affiche l'IMC et un message d'interprétation.
  3. Affiche une réserve claire : cette information ne doit pas servir à un autodiagnostic, il faut préférer l'avis d'un médecin.
Indice léger

La question 1 et la question 2 ne demandent pas la même chose. La fonction calcule et renvoie, elle n'affiche rien et ne demande rien à l'utilisateur. Tout ce qui est saisie et affichage se passe en dehors d'elle.

Indice plus précis

def imc(poids: float, taille: float) -> float: renvoie poids / (taille * taille), avec la taille en mètres. Le programme de la question 2 appelle cette fonction, range le résultat dans une variable, puis enchaîne des elif sur cette variable. Si tu as mis un print dans imc, relis le piège return contre print.

6. Une fonction peut en appeler une autre

La définition d'une fonction peut utiliser une autre fonction, déjà existante ou écrite par toi. C'est ainsi qu'on construit, à partir de briques simples, des fonctions plus puissantes.

def carre(x: float) -> float:
    """Renvoie le carré de x"""
    return x * x

def somme_carres(a: float, b: float) -> float:
    """Renvoie la somme des carrés de a et b"""
    return carre(a) + carre(b)

print(somme_carres(3, 4))   # 25

Exercice hors ligne : une forêt de sapins (turtle)

Le module turtle ouvre une fenêtre de dessin : cet exercice est à réaliser dans un éditeur Python (Thonny, IDLE...), pas dans le navigateur.

La fonction triangle ci-dessous dessine un triangle isocèle de base horizontale, de sommet \((x, y)\).

import turtle

def triangle(tortue: turtle.Turtle, x: int, y: int, cote: int, couleur: str) -> None:
    """Dessine un triangle isocèle de base horizontale, de sommet (x, y)"""
    tortue.penup()
    tortue.goto(x, y)
    tortue.pendown()
    tortue.begin_fill()
    tortue.color('black', couleur)
    tortue.left(210)
    tortue.forward(cote)
    tortue.left(150)
    tortue.forward(cote * 1.732)
    tortue.left(150)
    tortue.forward(cote)
    tortue.left(210)
    tortue.end_fill()
  1. En utilisant triangle, écris une fonction sapin(tortue, x, y) qui empile quelques triangles pour dessiner un sapin.
  2. Écris une fonction foret(tortue, n) qui dessine n sapins à des positions choisies aléatoirement.

Tu sais maintenant écrire une fonction. Reste à s'assurer qu'elle est correcte : c'est l'objet de la page Spécification et tests, qui reprend la docstring et introduit les tests automatiques.