0%
Fonctions en Python : organisation et réutilisation du code

Fonctions

Structurez et réutilisez votre code efficacement

10-15 min

Fonctions en Python : organisation et réutilisation du code

Les fonctions sont l’un des concepts fondamentaux de la programmation. Elles permettent de regrouper des séquences d’instructions en blocs réutilisables, d’organiser le code en composants logiques et d’éviter les répétitions. Dans ce tutoriel, nous explorerons en profondeur les fonctions en Python, de la syntaxe de base aux concepts avancés.

Qu’est-ce qu’une fonction ?

Une fonction est un bloc de code nommé qui effectue une tâche spécifique. Les fonctions peuvent accepter des données d’entrée (paramètres), effectuer des opérations sur ces données, et renvoyer un résultat. Elles permettent de :

  • Réutiliser du code sans avoir à le réécrire
  • Simplifier la maintenance en compartimentant la logique
  • Améliorer la lisibilité en divisant un problème complexe en sous-problèmes
  • Faciliter les tests en isolant des comportements spécifiques

Définir une fonction simple

En Python, les fonctions sont définies avec le mot-clé def suivi du nom de la fonction et de parenthèses contenant éventuellement des paramètres.

def saluer():
    """Cette fonction affiche un simple message de salutation."""
    print("Bonjour, monde !")

# Appel de la fonction
saluer()  # Affiche: Bonjour, monde !

Anatomie d’une fonction

  • Définition : Commence par def, suivi du nom de la fonction et de parenthèses
  • Docstring : Chaîne de documentation (optionnelle mais recommandée) entre triples guillemets
  • Corps : Code indenté qui constitue la fonction
  • Instruction return : Permet de renvoyer une valeur (optionnel)

Paramètres et arguments

Les paramètres permettent aux fonctions d’accepter des données en entrée, les rendant plus flexibles et réutilisables.

Paramètres positionnels

def saluer_personne(prenom, nom):
    """Salue une personne par son nom complet."""
    print(f"Bonjour, {prenom} {nom} !")

# Appel avec des arguments positionnels
saluer_personne("Marie", "Dupont")  # Affiche: Bonjour, Marie Dupont !

Paramètres par défaut

Les paramètres par défaut définissent une valeur qui sera utilisée si aucun argument n’est fourni.

def saluer_avec_formule(nom, formule="Bonjour"):
    """Salue avec une formule personnalisable."""
    print(f"{formule}, {nom} !")

# Appels de la fonction
saluer_avec_formule("Jean")  # Utilise la formule par défaut
# Affiche: Bonjour, Jean !

saluer_avec_formule("Pierre", "Salut")  # Remplace la formule par défaut
# Affiche: Salut, Pierre !

Arguments nommés

Les arguments nommés permettent de spécifier explicitement quel paramètre reçoit quelle valeur.

def decrire_personne(nom, age, profession, ville):
    """Affiche une description d'une personne."""
    print(f"{nom}, {age} ans, est {profession} à {ville}.")

# Appel avec arguments nommés (ordre différent de la définition)
decrire_personne(
    ville="Paris", 
    profession="ingénieure", 
    nom="Sophie", 
    age=32
)
# Affiche: Sophie, 32 ans, est ingénieure à Paris.

Nombre variable d’arguments

Python permet de définir des fonctions qui acceptent un nombre variable d’arguments.

Arguments positionnels variables (*args)

def calculer_somme(*nombres):
    """Calcule la somme de tous les nombres fournis."""
    resultat = 0
    for nombre in nombres:
        resultat += nombre
    return resultat

# Appels avec différents nombres d'arguments
print(calculer_somme(1, 2))  # 3
print(calculer_somme(1, 2, 3, 4, 5))  # 15
print(calculer_somme())  # 0

Arguments nommés variables (**kwargs)

def afficher_informations(**infos):
    """Affiche toutes les informations fournies sous forme de paires clé-valeur."""
    for cle, valeur in infos.items():
        print(f"{cle}: {valeur}")

# Appel avec plusieurs arguments nommés
afficher_informations(
    nom="Alice", 
    age=28, 
    email="alice@example.com"
)
# Affiche:
# nom: Alice
# age: 28
# email: alice@example.com

Combinaison de types d’arguments

def fonction_complete(arg1, arg2, *args, kwarg1="défaut", **kwargs):
    """Démontre tous les types d'arguments possibles."""
    print(f"arg1: {arg1}")
    print(f"arg2: {arg2}")
    print(f"args: {args}")
    print(f"kwarg1: {kwarg1}")
    print(f"kwargs: {kwargs}")

fonction_complete(1, 2, 3, 4, 5, kwarg1="personnalisé", x=10, y=20)
# Affiche:
# arg1: 1
# arg2: 2
# args: (3, 4, 5)
# kwarg1: personnalisé
# kwargs: {'x': 10, 'y': 20}

Valeurs de retour

Une fonction peut renvoyer une valeur à l’aide de l’instruction return. Si aucune instruction return n’est présente, la fonction renvoie implicitement None.

Retourner une valeur simple

def carre(nombre):
    """Retourne le carré d'un nombre."""
    return nombre ** 2

resultat = carre(4)
print(resultat)  # 16

Retourner plusieurs valeurs

def obtenir_dimensions():
    """Retourne la largeur et la hauteur d'un écran."""
    largeur = 1920
    hauteur = 1080
    return largeur, hauteur  # Retourne un tuple

dimensions = obtenir_dimensions()
print(dimensions)  # (1920, 1080)

# Déballage du tuple retourné
largeur, hauteur = obtenir_dimensions()
print(f"Largeur: {largeur}, Hauteur: {hauteur}")  # Largeur: 1920, Hauteur: 1080

Retourner différentes valeurs selon les conditions

def analyser_nombre(n):
    """Analyse un nombre et retourne différentes informations selon sa valeur."""
    if n < 0:
        return "négatif"
    elif n == 0:
        return "zéro"
    else:
        return "positif", "pair" if n % 2 == 0 else "impair"

# Appels avec différentes valeurs
print(analyser_nombre(-5))  # négatif
print(analyser_nombre(0))   # zéro
print(analyser_nombre(7))   # ('positif', 'impair')
print(analyser_nombre(8))   # ('positif', 'pair')

Sortie anticipée avec return

def diviser_securise(a, b):
    """Effectue une division sécurisée."""
    if b == 0:
        print("Division par zéro impossible")
        return None  # Sortie anticipée
    return a / b

print(diviser_securise(10, 2))  # 5.0
print(diviser_securise(10, 0))  # None (après avoir affiché le message d'erreur)

Portée des variables et espaces de noms

La portée d’une variable détermine où cette variable est accessible dans votre code.

Variables locales et globales

# Variable globale
x = 10

def modifier_variable():
    # Variable locale
    y = 5
    print(f"Dans la fonction, x = {x}")  # Accès à la variable globale x
    print(f"Dans la fonction, y = {y}")

modifier_variable()
print(f"Hors fonction, x = {x}")
# print(f"Hors fonction, y = {y}")  # Erreur : y n'est pas défini

Modifier une variable globale

Pour modifier une variable globale à l’intérieur d’une fonction, il faut utiliser le mot-clé global.

compteur = 0

def incrementer():
    global compteur  # Déclare que nous utilisons la variable globale
    compteur += 1
    print(f"Compteur = {compteur}")

incrementer()  # Compteur = 1
incrementer()  # Compteur = 2

Variables non locales (closures)

Dans les fonctions imbriquées, le mot-clé nonlocal permet d’accéder à des variables de la fonction englobante.

def creer_compteur():
    """Crée et retourne une fonction de comptage."""
    count = 0
    
    def incrementer():
        nonlocal count  # Accès à la variable de la fonction englobante
        count += 1
        return count
    
    return incrementer

mon_compteur = creer_compteur()
print(mon_compteur())  # 1
print(mon_compteur())  # 2
print(mon_compteur())  # 3

Fonctions Lambda (fonctions anonymes)

Les fonctions lambda sont des fonctions anonymes, définies en une seule ligne avec une syntaxe concise.

Syntaxe des fonctions lambda

# Fonction standard
def carre(x):
    return x ** 2

# Équivalent avec lambda
carre_lambda = lambda x: x ** 2

print(carre(5))        # 25
print(carre_lambda(5)) # 25

Utilisation typique avec les fonctions d’ordre supérieur

Les lambdas sont souvent utilisées avec des fonctions comme map(), filter() et sorted().

# Utilisation avec map() pour transformer une liste
nombres = [1, 2, 3, 4, 5]
carres = list(map(lambda x: x ** 2, nombres))
print(carres)  # [1, 4, 9, 16, 25]

# Utilisation avec filter() pour filtrer une liste
nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pairs = list(filter(lambda x: x % 2 == 0, nombres))
print(pairs)  # [2, 4, 6, 8, 10]

# Utilisation avec sorted() pour trier une liste
etudiants = [
    {"nom": "Alice", "note": 85},
    {"nom": "Bob", "note": 92},
    {"nom": "Charlie", "note": 78}
]
etudiants_tries = sorted(etudiants, key=lambda etudiant: etudiant["note"], reverse=True)
for etudiant in etudiants_tries:
    print(f"{etudiant['nom']}: {etudiant['note']}")
# Bob: 92
# Alice: 85
# Charlie: 78

Limitations des fonctions lambda

Les fonctions lambda sont limitées à une seule expression et ne peuvent pas contenir d’instructions multiples ni de structures de contrôle complexes. Pour des fonctions plus complexes, il est préférable d’utiliser la syntaxe def.

Fonctions récursives

Une fonction récursive est une fonction qui s’appelle elle-même.

Exemple de base : factorielle

def factorielle(n):
    """Calcule la factorielle de n de manière récursive."""
    if n <= 1:  # Cas de base
        return 1
    else:  # Appel récursif
        return n * factorielle(n - 1)

print(factorielle(5))  # 120 (5 * 4 * 3 * 2 * 1)

Exemple avancé : parcours d’une structure imbriquée

def parcourir_structure(structure, niveau=0):
    """Parcourt récursivement une structure de données imbriquée."""
    # Afficher la structure actuelle avec indentation selon le niveau
    indentation = "  " * niveau
    print(f"{indentation}Niveau {niveau}: {structure}")
    
    # Si la structure est un dictionnaire ou une liste, parcourir récursivement
    if isinstance(structure, dict):
        for cle, valeur in structure.items():
            print(f"{indentation}  Clé: {cle}")
            parcourir_structure(valeur, niveau + 1)
    elif isinstance(structure, list):
        for i, item in enumerate(structure):
            print(f"{indentation}  Index: {i}")
            parcourir_structure(item, niveau + 1)

# Exemple d'utilisation
structure_exemple = {
    "personne": {
        "nom": "Dupont",
        "adresses": [
            {"type": "domicile", "ville": "Paris"},
            {"type": "travail", "ville": "Lyon"}
        ]
    }
}

parcourir_structure(structure_exemple)

Limites de la récursion

Python limite la profondeur de récursion pour éviter les débordements de pile. Par défaut, cette limite est généralement de 1000 appels.

import sys
print(sys.getrecursionlimit())  # Affiche la limite actuelle (généralement 1000)

# Pour modifier la limite (à utiliser avec précaution)
# sys.setrecursionlimit(2000)

Pour éviter les problèmes de récursion profonde, certains algorithmes récursifs peuvent être réécrits de manière itérative ou en utilisant des techniques comme la mémoïsation.

Décorateurs de fonctions

Les décorateurs sont un concept avancé qui permet de modifier ou d’étendre le comportement des fonctions.

Concept de base

def mon_decorateur(fonction):
    def wrapper():
        print("Avant l'appel de la fonction")
        fonction()
        print("Après l'appel de la fonction")
    return wrapper

@mon_decorateur
def dire_bonjour():
    print("Bonjour !")

dire_bonjour()
# Affiche:
# Avant l'appel de la fonction
# Bonjour !
# Après l'appel de la fonction

Décorateur préservant les arguments

def decorateur_avec_arguments(fonction):
    def wrapper(*args, **kwargs):
        print(f"Arguments: {args}, Kwargs: {kwargs}")
        resultat = fonction(*args, **kwargs)
        print(f"Résultat: {resultat}")
        return resultat
    return wrapper

@decorateur_avec_arguments
def addition(a, b):
    return a + b

addition(3, 5)
# Affiche:
# Arguments: (3, 5), Kwargs: {}
# Résultat: 8

Décorateurs avec des paramètres

def repeter(n):
    def decorateur(fonction):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                resultat = fonction(*args, **kwargs)
            return resultat
        return wrapper
    return decorateur

@repeter(3)
def dire_bonjour(nom):
    print(f"Bonjour, {nom} !")
    return nom

dire_bonjour("Alice")
# Affiche 3 fois:
# Bonjour, Alice !

Documentation et annotations de type

Docstrings

Les docstrings (chaînes de documentation) permettent de documenter vos fonctions.

def calculer_moyenne(nombres):
    """
    Calcule la moyenne arithmétique d'une liste de nombres.
    
    Args:
        nombres (list): Une liste de nombres (int ou float)
        
    Returns:
        float: La moyenne des nombres
        
    Raises:
        ZeroDivisionError: Si la liste est vide
        
    Examples:
        >>> calculer_moyenne([1, 2, 3, 4, 5])
        3.0
    """
    return sum(nombres) / len(nombres)

Les docstrings peuvent être consultées avec la fonction help() :

help(calculer_moyenne)

Annotations de type (Python 3.5+)

Les annotations de type améliorent la lisibilité et permettent la vérification statique du code.

def saluer(nom: str, age: int = 30) -> str:
    """Retourne un message de salutation personnalisé."""
    return f"Bonjour {nom}, vous avez {age} ans !"

# Les annotations sont stockées dans l'attribut __annotations__
print(saluer.__annotations__)
# {'nom': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

Pour les types plus complexes, utilisez le module typing :

from typing import List, Dict, Tuple, Optional, Union, Callable

def traiter_donnees(
    valeurs: List[int],
    config: Dict[str, str],
    limites: Tuple[int, int],
    callback: Optional[Callable[[int], bool]] = None
) -> Union[int, str]:
    """Traite des données avec des types complexes."""
    # Implémentation...
    return 0

Bonnes pratiques pour les fonctions

Principes généraux

  1. Une fonction, une responsabilité : Chaque fonction devrait avoir un objectif clair et unique.

  2. Noms descriptifs : Choisissez des noms qui expliquent ce que fait la fonction, pas comment elle le fait.

    # Bon
    def calculer_moyenne(nombres):
        return sum(nombres) / len(nombres)
    
    # À éviter
    def fonction1(liste):
        return sum(liste) / len(liste)
  3. Taille raisonnable : Les fonctions devraient être relativement courtes (généralement moins de 20-30 lignes).

  4. Paramètres par défaut immuables : Évitez d’utiliser des objets mutables comme valeurs par défaut.

    # Problématique
    def ajouter_element(element, liste=[]):
        liste.append(element)
        return liste
    
    # Meilleure approche
    def ajouter_element(element, liste=None):
        if liste is None:
            liste = []
        liste.append(element)
        return liste
  5. Documentation claire : Utilisez des docstrings pour expliquer le but, les paramètres et les valeurs de retour.

Convention de nommage

Suivez la PEP 8 pour les conventions de nommage :

  • Utilisez des noms en minuscules séparés par des underscores (snake_case)
  • Préférez les verbes pour les fonctions qui effectuent des actions
  • Soyez cohérent dans vos conventions
# Bon
def calculer_moyenne(nombres):
    pass

def obtenir_utilisateur_par_id(user_id):
    pass

def est_valide(donnee):
    pass

Exemple complet : Application des fonctions

Voici un exemple qui combine plusieurs concepts de fonctions pour créer un système simple de gestion de tâches :

from typing import List, Dict, Optional, Tuple, Callable
import time

# Types personnalisés
Tache = Dict[str, any]
ListeTaches = List[Tache]

def creer_tache(
    titre: str, 
    description: str = "", 
    priorite: int = 1
) -> Tache:
    """
    Crée une nouvelle tâche avec un horodatage automatique.
    
    Args:
        titre: Le titre de la tâche
        description: Une description détaillée (optionnelle)
        priorite: Niveau de priorité (1-5, 5 étant le plus important)
        
    Returns:
        Un dictionnaire représentant la tâche
    """
    return {
        "id": int(time.time()),
        "titre": titre,
        "description": description,
        "priorite": max(1, min(5, priorite)),  # Garantit que la priorité est entre 1 et 5
        "termine": False,
        "date_creation": time.strftime("%Y-%m-%d %H:%M:%S")
    }

def ajouter_tache(
    liste_taches: ListeTaches, 
    tache: Tache
) -> ListeTaches:
    """Ajoute une tâche à la liste et retourne la liste mise à jour."""
    liste_taches.append(tache)
    return liste_taches

def marquer_comme_termine(
    liste_taches: ListeTaches, 
    tache_id: int
) -> Tuple[bool, str]:
    """
    Marque une tâche comme terminée.
    
    Returns:
        Un tuple (succès, message)
    """
    for tache in liste_taches:
        if tache["id"] == tache_id:
            tache["termine"] = True
            return True, f"Tâche '{tache['titre']}' marquée comme terminée"
    return False, "Tâche non trouvée"

def filtrer_taches(
    liste_taches: ListeTaches, 
    termine: Optional[bool] = None, 
    priorite_min: int = 0
) -> ListeTaches:
    """
    Filtre les tâches selon plusieurs critères.
    
    Args:
        liste_taches: Liste des tâches à filtrer
        termine: Si spécifié, filtre les tâches selon leur état
        priorite_min: Filtre les tâches avec une priorité supérieure ou égale
        
    Returns:
        Une nouvelle liste contenant les tâches filtrées
    """
    resultat = []
    for tache in liste_taches:
        # Vérifier l'état de la tâche si le paramètre est spécifié
        if termine is not None and tache["termine"] != termine:
            continue
        
        # Vérifier la priorité minimale
        if tache["priorite"] < priorite_min:
            continue
            
        resultat.append(tache)
    return resultat

def afficher_taches(
    liste_taches: ListeTaches,
    formatter: Callable[[Tache], str] = None
) -> None:
    """
    Affiche une liste de tâches.
    
    Args:
        liste_taches: Liste des tâches à afficher
        formatter: Fonction optionnelle pour formater chaque tâche
    """
    if not liste_taches:
        print("Aucune tâche à afficher.")
        return
    
    # Formatter par défaut
    if formatter is None:
        def formatter(tache):
            statut = "✓" if tache["termine"] else "☐"
            return f"{statut} [{tache['priorite']}] {tache['titre']}"
    
    # Afficher chaque tâche
    for i, tache in enumerate(liste_taches, 1):
        print(f"{i}. {formatter(tache)}")

# Utilisation de nos fonctions
if __name__ == "__main__":
    mes_taches = []
    
    # Ajouter quelques tâches
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Apprendre les fonctions Python", "Tutoriel complet", 4)
    )
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Faire les courses")
    )
    mes_taches = ajouter_tache(
        mes_taches, 
        creer_tache("Répondre aux emails", priorite=5)
    )
    
    # Afficher les tâches
    print("Toutes les tâches:")
    afficher_taches(mes_taches)
    
    # Marquer une tâche comme terminée
    succes, message = marquer_comme_termine(mes_taches, mes_taches[1]["id"])
    print(f"\nRésultat: {message}")
    
    # Filtrer et afficher les tâches non terminées avec priorité >= 4
    taches_importantes = filtrer_taches(mes_taches, termine=False, priorite_min=4)
    print("\nTâches importantes non terminées:")
    afficher_taches(taches_importantes)
    
    # Utiliser un formateur personnalisé
    print("\nAffichage détaillé:")
    afficher_taches(mes_taches, lambda t: f"{t['titre']} - Priorité: {t['priorite']} - Créée le: {t['date_creation']}")

Conclusion

Les fonctions sont l’un des piliers de la programmation en Python. Elles permettent de structurer votre code, de le rendre plus modulaire, plus lisible et plus facile à maintenir. En maîtrisant les différentes techniques présentées dans ce tutoriel, vous pourrez :

  • Diviser des problèmes complexes en sous-problèmes gérables
  • Éviter la duplication de code
  • Créer des bibliothèques de fonctions réutilisables
  • Écrire du code plus expressif et maintenable

Dans le prochain tutoriel, nous explorerons les modules et packages en Python, qui vous permettront d’organiser vos fonctions et classes en unités logiques et réutilisables.


Besoin de réviser les structures de données? Consultez notre tutoriel sur les structures de données en Python.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !