Fonctions d'Ordre Supérieur et Décorateurs en Python

Comprendre les Fonctions en tant qu'Objets

En Python, les fonctions sont des citoyens de première classe. Cela signifie qu'elles peuvent être traitées comme n'importe quelle autre varible, stockées dans des structures de données, passées comme arguments à d'autres fonctions, ou retournées comme valeurs. Par exemple, la fonction native min, qui trouve l'élément le plus petit dans un ensemble, peut être assignée à une autre variable :


# Utilisation standard de min
resultat_min = min(10, 3, 55, 4)
print(f"Le minimum est : {resultat_min}") # Affiche : Le minimum est : 3

# Assignation de la fonction min à une variable
fonction_minimale = min
print(f"Le type de 'fonction_minimale' est : {type(fonction_minimale)}") # Affiche : Le type de 'fonction_minimale' est : <class 'builtin_function_or_method'>

# Utilisation de la variable comme une fonction
nouveau_resultat = fonction_minimale(7, 2, 9)
print(f"Nouveau résultat minimal : {nouveau_resultat}") # Affiche : Nouveau résultat minimal : 2

Cette flexibilité est la pierre angulaire des fonctions d'ordre supérieur et des décorateurs.

Fonctions d'Ordre Supérieur Intégrées

Les fonctions d'ordre supérieur sont des fonctions qui acceptent une ou plusieurs fonctions comme arguments, ou qui retournent une fonction comme résultat. Python propose plusieurs fonctions d'ordre supérieur très utiles.

La fonction map()

La fonction map() applique une fonction donnée à chaque élément d'un itérable (comme une liste ou un tuple) et retourne un itérateur produisant les résultats. C'est un moyen efficace d'effectuer la même opération sur plusieurs éléments.


# script_map.py
def doubler_nombre(valeur):
    """Retourne le double de la valeur d'entrée."""
    return valeur * 2

nombres_initiaux = [1, 2, 3, 4, 5]
# Applique doubler_nombre à chaque élément
nombres_doubles = list(map(doubler_nombre, nombres_initiaux))
print(f"Nombres initiaux : {nombres_initiaux}")
print(f"Nombres doublés : {nombres_doubles}")
# Affiche :
# Nombres initiaux : [1, 2, 3, 4, 5]
# Nombres doublés : [2, 4, 6, 8, 10]

La fonction filter()

La fonction filter() construit un itérateur à partir des éléments d'un itérable pour lesquels une fonction donnée retourne True. Elle permet de sélectionner des éléments qui répondent à un certain critère.


# script_filter.py
def est_pair(nombre):
    """Vérifie si un nombre est pair."""
    return nombre % 2 == 0

liste_de_nombres = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Filtre les nombres pour ne garder que les pairs
nombres_pairs = list(filter(est_pair, liste_de_de_nombres))
print(f"Liste complète : {liste_de_nombres}")
print(f"Nombres pairs : {nombres_pairs}")
# Affiche :
# Liste complète : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Nombres pairs : [0, 2, 4, 6, 8, 10]

La fonction functools.reduce()

La fonction reduce() (située dans le module functools en Python 3) applique une fonction à une paire d'éléments d'un itérable de manière cumulative, de gauche à droite, pour réduire l'itérable à une seule valeur.


# script_reduce.py
from functools import reduce

def produit_deux_nombres(a, b):
    """Calcule le produit de deux nombres."""
    return a * b

# Calcule le produit cumulatif des nombres de 1 à 5 (1*2*3*4*5)
resultat_produit = reduce(produit_deux_nombres, range(1, 6))
print(f"Produit des nombres de 1 à 5 : {resultat_produit}")
# Affiche : Produit des nombres de 1 à 5 : 120

# Autre exemple : concaténation de chaînes
mots = ["Bonjour", "le", "monde"]
phrase = reduce(lambda x, y: x + " " + y, mots)
print(f"Phrase construite : {phrase}")
# Affiche : Phrase construite : Bonjour le monde

La fonction sorted()

La fonction sorted() retourne une nouvelle liste triée à partir des éléments d'un itérable. Elle accepte un argument optionnel key qui est une fonction à appliquer à chaque élément avant de le comparer pour le tri. L'argument reverse permet un tri descendant.


# script_sorted.py
noms = ["alice", "Bob", "charlie", "David"]

# Tri par défaut (sensible à la casse, basé sur l'ordre ASCII)
noms_tries_defaut = sorted(noms)
print(f"Tri par défaut : {noms_tries_defaut}")
# Affiche : Tri par défaut : ['Bob', 'David', 'alice', 'charlie']

# Tri insensible à la casse en utilisant 'key'
noms_tries_insensible = sorted(noms, key=str.lower)
print(f"Tri insensible à la casse : {noms_tries_insensible}")
# Affiche : Tri insensible à la casse : ['alice', 'Bob', 'charlie', 'David']

# Tri inverse de chaînes
noms_tries_inverse = sorted(noms, key=str.lower, reverse=True)
print(f"Tri insensible à la casse inversé : {noms_tries_inverse}")
# Affiche : Tri insensible à la casse inversé : ['David', 'charlie', 'Bob', 'alice']

# Tri de listes imbriquées par le deuxième élément
produits = [["pomme", 2.50, 15], ["banane", 1.20, 30], ["orange", 3.00, 10]]
# Trier par prix (deuxième élément)
produits_par_prix = sorted(produits, key=lambda p: p[1])
print(f"Produits triés par prix : {produits_par_prix}")
# Affiche : Produits triés par prix : [['banane', 1.2, 30], ['pomme', 2.5, 15], ['orange', 3.0, 10]]

Fonctions Anonymes Lambda

Les fonctions lambda sont de petites fonctions anonymes qui peuvent avoir n'importe quel nombre d'arguments, mais un seul énoncé. Elles sont souvent utilisées comme arguments pour des fonctions d'ordre supérieur comme map(), filter() ou sorted(), lorsqu'une fonction simple est nécessaire pour une courte période.


# script_lambda.py
from functools import reduce

# Calculer les carrés avec lambda et map
carres = list(map(lambda x: x**2, [1, 2, 3, 4]))
print(f"Carrés : {carres}") # Affiche : Carrés : [1, 4, 9, 16]

# Somme cumulative avec lambda et reduce
somme_cumulative = reduce(lambda x, y: x + y, range(1, 6))
print(f"Somme cumulative de 1 à 5 : {somme_cumulative}") # Affiche : Somme cumulative de 1 à 5 : 15

# Filtrer les mots courts avec lambda et filter
mots_longs = list(filter(lambda mot: len(mot) > 4, ["chat", "maison", "soleil", "ami"]))
print(f"Mots de plus de 4 lettres : {mots_longs}") # Affiche : Mots de plus de 4 lettres : ['maison', 'soleil']

Décorateurs

Les décorateurs Python sont une forme de "sucre syntaxique" qui permet de modifier le comportement de fonctions ou de classes sans altérer leur code source. Ils sont implémentés comme des fonctions d'ordre supérieur.

Fermetures (Closures)

Avant d'aborder les décorateurs, il est important de comprendre le concept de fermeture. Une fermeture se produit lorsqu'une fonction interne (ou imbriquée) est retournée par une fonction externe, et que cette fonction interne conserve l'accès aux variables locales de la fonction externe même après l'exécution de celle-ci.


# script_fermeture.py
def creer_compteur(increment=1):
    """
    Fonction externe qui retourne une fonction interne (fermeture).
    La fonction interne "se souvient" de la valeur d'increment.
    """
    compteur_interne = 0 # Variable locale à creer_compteur

    def incrementer():
        nonlocal compteur_interne # Indique que nous modifions la variable de la fonction externe
        compteur_interne += increment
        return compteur_interne
    return incrementer

# Création de deux compteurs différents
mon_compteur = creer_compteur(2)
ton_compteur = creer_compteur(5)

print(f"Mon compteur : {mon_compteur()}") # Affiche : Mon compteur : 2
print(f"Mon compteur : {mon_compteur()}") # Affiche : Mon compteur : 4
print(f"Ton compteur : {ton_compteur()}") # Affiche : Ton compteur : 5
print(f"Mon compteur : {mon_compteur()}") # Affiche : Mon compteur : 6

Décorateurs Simples

Les décorateurs sont couramment utilisés pour ajouter des fonctionnalités transversales (comme la journalisation, la gestion des permissions, la mesure du temps d'exécution) à plusieurs fonctions sans dupliquer le code. Un décorateur prend une fonction en argument et retourne une nouvelle fonction qui enveloppe l'originale.


# script_decorateur_simple.py
def journaliser_appel(fonction_a_decorer):
    """
    Un décorateur simple qui ajoute un message de journalisation
    avant et après l'exécution de la fonction décorée.
    """
    def enveloppeur():
        print("--- Début de l'exécution de la fonction ---")
        fonction_a_decorer() # Appel de la fonction originale
        print("--- Fin de l'exécution de la fonction ---")
    return enveloppeur

@journaliser_appel # Syntaxe du décorateur
def dire_bonjour():
    print("Bonjour le monde !")

@journaliser_appel
def afficher_message_secret():
    print("Ceci est un message secret.")

print("Appel de dire_bonjour :")
dire_bonjour()

print("\nAppel de afficher_message_secret :")
afficher_message_secret()
# Exécution :
# Appel de dire_bonjour :
# --- Début de l'exécution de la fonction ---
# Bonjour le monde !
# --- Fin de l'exécution de la fonction ---
#
# Appel de afficher_message_secret :
# --- Début de l'exécution de la fonction ---
# Ceci est un message secret.
# --- Fin de l'exécution de la fonction ---

Décorateurs avec Arguments pour la Fonction Décorée

Si la fonction décorée accepte des arguments, l'enveloppeur du décorateur doit être capable de les gérer. On utilise pour cela les arguments variables *args et **kwargs.


# script_decorateur_avec_args.py
import time

def mesurer_temps_execution(fonction_a_decorer):
    """
    Décorateur qui mesure le temps d'exécution d'une fonction.
    Gère les arguments de la fonction décorée.
    """
    def enveloppeur(*args, **kwargs):
        temps_debut = time.time()
        resultat = fonction_a_decorer(*args, **kwargs) # Passe les arguments à la fonction originale
        temps_fin = time.time()
        duree = temps_fin - temps_debut
        print(f"La fonction '{fonction_a_decorer.__name__}' a pris {duree:.4f} secondes.")
        return resultat # Retourne le résultat de la fonction originale
    return enveloppeur

@mesurer_temps_execution
def calculer_somme(a, b):
    time.sleep(0.1) # Simule un travail long
    return a + b

@mesurer_temps_execution
def saluer(nom, prenom=""):
    time.sleep(0.05)
    return f"Salut, {nom} {prenom}!"

somme = calculer_somme(10, 20)
print(f"Somme : {somme}")

message = saluer("Dupont", prenom="Jean")
print(f"Message : {message}")

Décorateurs avec Arguments pour le Décorateur Lui-même

Il est possible de passer des arguments au décorateur. Cela nécessite une couche d'imbrication supplémentaire : la fonction externe prend les arguments du décorateur et retourne le décorateur réel (qui est une fonction prenant la fonction à décorer).


# script_decorateur_avec_args_decorateur.py
def autoriser_acces(role_requis):
    """
    Décorateur avec argument qui vérifie si l'utilisateur a le rôle requis.
    """
    def create_decorateur(fonction_a_decorer):
        def enveloppeur(*args, **kwargs):
            # Ici, on simule une vérification de rôle
            roles_utilisateur = ["admin", "editeur"] # Supposons que l'utilisateur a ces rôles
            if role_requis in roles_utilisateur:
                print(f"Accès autorisé pour le rôle '{role_requis}'.")
                return fonction_a_decorer(*args, **kwargs)
            else:
                print(f"Accès refusé. Rôle '{role_requis}' requis.")
                return None # Ou lever une exception
        return enveloppeur
    return create_decorateur

@autoriser_acces("admin")
def supprimer_donnees_sensibles():
    print("Données sensibles supprimées avec succès.")

@autoriser_acces("editeur")
def editer_article(article_id):
    print(f"Article {article_id} édité.")

@autoriser_acces("viewer")
def voir_rapports():
    print("Rapports affichés.")

print("Essai de suppression :")
supprimer_donnees_sensibles()

print("\nEssai d'édition :")
editer_article(123)

print("\nEssai de consultation :")
voir_rapports()

functools.wraps pour Préserver les Métadonnées

Un problème avec les décorateurs est qu'ils remplacent la fonction originale par la fonction enveloppe. Cela signifie que les métadonnées de la fonction originale (comme son nom __name__, sa chaîne de documentation __doc__, etc.) sont perdues et remplacées par celles de la fonction enveloppe (souvent appelée wrapper). Cela peut poser problème pour des outils d'introspection ou de débogage.

Le module functools fournit le décorateur @functools.wraps pour résoudre ce problème. Il copie automatiquement les attributs pertinents de la fonction originale vers la fonction enveloppe.


# script_wraps.py
import functools

def decorateur_avec_wraps(fonction_a_decorer):
    """
    Décorateur qui utilise functools.wraps pour préserver les métadonnées.
    """
    @functools.wraps(fonction_a_decorer) # Applique wraps ici
    def enveloppeur(*args, **kwargs):
        print(f"Pré-traitement avant '{fonction_a_decorer.__name__}'")
        resultat = fonction_a_decorer(*args, **kwargs)
        print(f"Post-traitement après '{fonction_a_decorer.__name__}'")
        return resultat
    return enveloppeur

@decorateur_avec_wraps
def ma_fonction_originale(arg1, arg2):
    """Docstring de ma_fonction_originale."""
    return f"Traitement de {arg1} et {arg2}"

print(f"Nom de la fonction décorée : {ma_fonction_originale.__name__}")
print(f"Docstring de la fonction décorée : {ma_fonction_originale.__doc__}")
print(ma_fonction_originale("valeur_A", "valeur_B"))

# Sans @functools.wraps, __name__ serait 'enveloppeur' et __doc__ serait vide ou la doc de l'enveloppeur.

Exercice : Décorateur de Mesure de Performance

Implémentez un décorateur @chronometrer qui mesure et affiche le temps d'exécution de toute fonction, en préservant ses métadonnées et gérant ses arguments. Le script ci-dessous doit s'exécuter correctement et afficher les temps d'exécution.


# -*- coding: utf-8 -*-
import time
import functools

def chronometrer(func):
    """
    Décorateur qui mesure le temps d'exécution d'une fonction et l'affiche.
    Utilise functools.wraps pour préserver les métadonnées de la fonction décorée.
    """
    @functools.wraps(func)
    def enveloppeur_performance(*args, **kwargs):
        start_time = time.perf_counter() # Utiliser perf_counter pour une meilleure précision
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print(f"La fonction '{func.__name__}' a été exécutée en {elapsed_time:.4f} secondes.")
        return result
    return enveloppeur_performance

# Test
@chronometrer
def traitement_rapide(x, y):
    time.sleep(0.01)
    return x * y

@chronometrer
def traitement_lent(liste_valeurs):
    total = 0
    for val in liste_valeurs:
        time.sleep(0.0001) # Petite pause pour simuler un calcul
        total += val
    return total

print("--- Démarrage des tests de performance ---")
res_rapide = traitement_rapide(5, 7)
print(f"Résultat rapide : {res_rapide}")

res_lent = traitement_lent(list(range(1, 101))) # Somme des nombres de 1 à 100
print(f"Résultat lent : {res_lent}")

if res_rapide != 35:
    print('Échec du test rapide!')
elif res_lent != 5050: # La somme de 1 à 100 est 5050
    print('Échec du test lent!')
else:
    print("Tests de performance réussis !")


Exercice : Décorateur de Journalisation

Créez un décorateur @enregistrer_journal qui enregistre l'heure d'exécution, le nom de la fonction et ses arguments chaque fois qu'elle est appelée. Le format du journal doit être : [AAAA-MM-JJ HH:MM:SS] Fonction: nom_func(args, kwargs).


# -*- coding: utf-8 -*-
import time
import functools

def enregistrer_journal(func):
    """
    Décorateur qui journalise les appels de fonction, y compris l'heure, le nom et les arguments.
    """
    @functools.wraps(func)
    def enveloppeur_journal(*args, **kwargs):
        result = func(*args, **kwargs) # Exécute la fonction originale
        heure_actuelle = time.strftime("%Y-%m-%d %H:%M:%S")

        # Formatter les arguments pour le journal
        args_repr = ", ".join(repr(a) for a in args)
        kwargs_repr = ", ".join(f"{k}={repr(v)}" for k, v in kwargs.items())
        
        if args_repr and kwargs_repr:
            tous_args = f"{args_repr}, {kwargs_repr}"
        elif args_repr:
            tous_args = args_repr
        elif kwargs_repr:
            tous_args = kwargs_repr
        else:
            tous_args = ""

        print(f"[{heure_actuelle}] Fonction: {func.__name__}({tous_args})")
        return result
    return enveloppeur_journal

@enregistrer_journal
def connexion_utilisateur(nom_utilisateur):
    time.sleep(0.02)
    print(f"Utilisateur '{nom_utilisateur}' connecté.")
    return True

@enregistrer_journal
def effectuer_transaction(montant, devise="EUR"):
    time.sleep(0.03)
    print(f"Transaction de {montant} {devise} effectuée.")
    return True

print("--- Démarrage des opérations ---")
connexion_utilisateur("admin_utilisateur")
effectuer_transaction(100.50, devise="USD")
effectuer_transaction(25)
print("--- Opérations terminées ---")


Étiquettes: Python Fonctions d'Ordre Supérieur décorateurs Lambda fermetures

Publié le 6 août à 17h15