{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Maîtriser les décorateurs Python\n", "\n", "Les décorateurs sont l'un des outils les plus puissants de Python. Ils permettent de modifier le comportement d'une fonction sans toucher à son code. Dans cet article, vous allez comprendre comment ils fonctionnent et comment créer les vôtres." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Les fonctions sont des objets\n", "\n", "Avant de parler de décorateurs, il faut comprendre un concept fondamental en Python : **les fonctions sont des objets**. Elles peuvent être passées en argument, retournées par d'autres fonctions, et assignées à des variables." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def saluer(nom: str) -> str:\n", " return f\"Bonjour {nom} !\"\n", "\n", "# Une fonction peut être assignée à une variable\n", "ma_fonction = saluer\n", "print(ma_fonction(\"Python\"))\n", "\n", "# Une fonction peut être passée en argument\n", "def executer(func, arg):\n", " return func(arg)\n", "\n", "print(executer(saluer, \"World\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Votre premier décorateur\n", "\n", "Un décorateur est simplement une fonction qui prend une fonction en argument et retourne une nouvelle fonction. Voici la structure de base :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from functools import wraps\n", "\n", "def mon_decorateur(func):\n", " @wraps(func) # Préserve les métadonnées de la fonction originale\n", " def wrapper(*args, **kwargs):\n", " print(f\"Avant l'appel de {func.__name__}\")\n", " resultat = func(*args, **kwargs)\n", " print(f\"Après l'appel de {func.__name__}\")\n", " return resultat\n", " return wrapper\n", "\n", "@mon_decorateur\n", "def dire_bonjour(nom: str) -> str:\n", " return f\"Bonjour {nom} !\"\n", "\n", "print(dire_bonjour(\"Alice\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cas pratique : mesurer le temps d'exécution\n", "\n", "Un cas d'usage classique des décorateurs est de mesurer le temps d'exécution d'une fonction. C'est particulièrement utile pour le **profiling** de votre code." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "from functools import wraps\n", "\n", "def timer(func):\n", " \"\"\"Mesure et affiche le temps d'exécution d'une fonction.\"\"\"\n", " @wraps(func)\n", " def wrapper(*args, **kwargs):\n", " start = time.perf_counter()\n", " result = func(*args, **kwargs)\n", " end = time.perf_counter()\n", " print(f\"{func.__name__} exécutée en {end - start:.4f}s\")\n", " return result\n", " return wrapper\n", "\n", "@timer\n", "def calcul_lent():\n", " \"\"\"Simule un calcul qui prend du temps.\"\"\"\n", " total = sum(i**2 for i in range(1_000_000))\n", " return total\n", "\n", "resultat = calcul_lent()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Décorateurs avec arguments\n", "\n", "Parfois, vous voulez paramétrer votre décorateur. Pour cela, il faut ajouter un niveau d'imbrication supplémentaire." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from functools import wraps\n", "\n", "def retry(max_attempts: int = 3, delay: float = 1.0):\n", " \"\"\"Réessaie une fonction en cas d'échec.\"\"\"\n", " def decorator(func):\n", " @wraps(func)\n", " def wrapper(*args, **kwargs):\n", " last_exception = None\n", " for attempt in range(1, max_attempts + 1):\n", " try:\n", " return func(*args, **kwargs)\n", " except Exception as e:\n", " last_exception = e\n", " print(f\"Tentative {attempt}/{max_attempts} échouée: {e}\")\n", " if attempt < max_attempts:\n", " time.sleep(delay)\n", " raise last_exception\n", " return wrapper\n", " return decorator\n", "\n", "import random\n", "\n", "@retry(max_attempts=3, delay=0.5)\n", "def operation_instable():\n", " \"\"\"Une opération qui échoue parfois.\"\"\"\n", " if random.random() < 0.7:\n", " raise ValueError(\"Oups, ça a raté !\")\n", " return \"Succès !\"\n", "\n", "try:\n", " print(operation_instable())\n", "except ValueError as e:\n", " print(f\"Échec définitif: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Décorateurs de classe\n", "\n", "Les décorateurs ne sont pas limités aux fonctions. Vous pouvez aussi décorer des classes entières, ou utiliser des classes comme décorateurs." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class CountCalls:\n", " \"\"\"Décorateur qui compte le nombre d'appels à une fonction.\"\"\"\n", " \n", " def __init__(self, func):\n", " self.func = func\n", " self.count = 0\n", " \n", " def __call__(self, *args, **kwargs):\n", " self.count += 1\n", " print(f\"{self.func.__name__} appelée {self.count} fois\")\n", " return self.func(*args, **kwargs)\n", "\n", "@CountCalls\n", "def fibonacci(n: int) -> int:\n", " if n < 2:\n", " return n\n", " return fibonacci(n - 1) + fibonacci(n - 2)\n", "\n", "# Attention : sans cache, fibonacci est appelée de nombreuses fois !\n", "print(f\"\\nRésultat: {fibonacci(10)}\")\n", "print(f\"Nombre total d'appels: {fibonacci.count}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bonus : les décorateurs built-in\n", "\n", "Python fournit plusieurs décorateurs très utiles dans la bibliothèque standard :" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from functools import lru_cache, cached_property\n", "from dataclasses import dataclass\n", "\n", "# @lru_cache : met en cache les résultats\n", "@lru_cache(maxsize=128)\n", "def fibonacci_cached(n: int) -> int:\n", " if n < 2:\n", " return n\n", " return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)\n", "\n", "print(f\"fibonacci(30) = {fibonacci_cached(30)}\")\n", "print(f\"Cache info: {fibonacci_cached.cache_info()}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# @property et @cached_property\n", "class Cercle:\n", " def __init__(self, rayon: float):\n", " self._rayon = rayon\n", " \n", " @property\n", " def rayon(self) -> float:\n", " return self._rayon\n", " \n", " @rayon.setter\n", " def rayon(self, value: float):\n", " if value < 0:\n", " raise ValueError(\"Le rayon doit être positif\")\n", " self._rayon = value\n", " \n", " @property\n", " def aire(self) -> float:\n", " import math\n", " return math.pi * self._rayon ** 2\n", "\n", "c = Cercle(5)\n", "print(f\"Rayon: {c.rayon}, Aire: {c.aire:.2f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conclusion\n", "\n", "Les décorateurs sont un outil essentiel en Python. Ils permettent de :\n", "\n", "- **Séparer les préoccupations** : logging, timing, validation, caching...\n", "- **Réutiliser du code** : un décorateur peut être appliqué à plusieurs fonctions\n", "- **Garder un code lisible** : la logique métier reste claire\n", "\n", "N'oubliez pas d'utiliser `@wraps` pour préserver les métadonnées de vos fonctions décorées !" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] } ], "metadata": { "ohcecours": { "title": "Maîtriser les décorateurs Python", "slug": "decorateurs-python", "description": "Les décorateurs sont l'un des outils les plus puissants de Python. Apprenez à les comprendre et à créer les vôtres pour écrire du code plus propre et réutilisable.", "author": "matthieu", "tags": ["python", "décorateurs", "clean-code", "avancé"], "difficulty": "intermédiaire", "duration_minutes": 25, "published": true }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 }