Décorateurs et gestionnaires de contexte en Python

Les deux outils pour envelopper les choses — écrivez votre propre décorateur (la fermeture wrapper, functools.wraps, les décorateurs à arguments) et votre propre gestionnaire de contexte (une classe enter/exit et le style générateur plus simple @contextmanager), chacun expliqué par une sortie que vous pouvez exécuter.

Écrivez vos propres décorateurs et gestionnaires de contexte Python : ce qu’est réellement un décorateur (une fonction qui enveloppe une fonction), la fermeture wrapper, functools.wraps pour préserver le nom et la docstring, les décorateurs qui prennent des arguments (le motif fabrique), et le motif de ressource avec l’instruction with — une classe gestionnaire de contexte avec enter/exit et le style générateur plus simple contextlib.contextmanager, plus contextlib.suppress.

Date de publication

18 juillet 2026

Modifié

18 juillet 2026

AstucePoints clés
  • Un décorateur est une fonction qui prend une fonction et en renvoie une nouvelle. La ligne @deco au-dessus d’un def n’est que du sucre syntaxique pour f = deco(f) — elle remplace votre fonction par une version enveloppée capable d’exécuter du code avant et après l’original. Utilisez-en un lorsque vous voulez le même comportement (chronométrage, journalisation, réessai, mise en cache) sur plusieurs fonctions sans éditer chacune.
  • @functools.wraps(fn) est l’unique habitude qui rend les décorateurs sûrs. Sans lui, la fonction décorée oublie ses propres __name__ et __doc__ (ils deviennent ceux du wrapper), ce qui casse l’introspection, help() et les débogueurs. Une ligne — @functools.wraps(fn) sur le wrapper — copie l’identité et définit __wrapped__.
  • Un décorateur qui prend des arguments, c’est trois fonctions imbriquées. @repeat(3) demande une fabrique (prend l’argument) qui renvoie un décorateur (prend la fonction) qui renvoie un wrapper (prend l’appel). Se tromper d’un cran dans cette imbrication est le bug classique du décorateur.
  • Un gestionnaire de contexte garantit la mise en place et le nettoyage autour d’un bloc — même en cas d’erreur. with exécute __enter__ à l’entrée et __exit__ à la sortie quelle que soit la manière dont le bloc se termine, si bien qu’un fichier est fermé, un verrou libéré, un chronomètre arrêté, même si le bloc lève une exception. Écrivez-en un comme une classe (__enter__/__exit__) ou, plus simplement, avec @contextlib.contextmanager (le code avant yield = mise en place, après = nettoyage, enveloppé dans try/finally).
  • Décorateur ou gestionnaire de contexte : réutiliser un comportement à travers des fonctions ou garantir mise en place/nettoyage autour d’un bloc. Ils partagent le modèle mental « envelopper quelque chose » — et un objet @contextmanager peut même servir de décorateur — mais cette seule distinction vous dit lequel choisir.

Introduction

Vous voulez ajouter le même comportement — chronométrage, journalisation, un réessai, un cache — à une douzaine de fonctions, sans copier-coller les quatre mêmes lignes dans chacune. Et vous voulez qu’une ressource — un fichier, une connexion à une base de données, un verrou, un chronomètre — soit nettoyée de façon fiable, même quand le code au milieu explose. Python a un outil pour chacune de ces tâches « d’enveloppe » : un décorateur enveloppe une fonction, et l’instruction with (adossée à un gestionnaire de contexte) enveloppe un bloc de code.

Un décorateur est une fonction qui prend une fonction et renvoie une nouvelle fonction — le @name que vous voyez sans cesse au-dessus de def. Un gestionnaire de contexte est un objet qui définit ce qui se passe quand vous entrez dans un bloc with et quand vous en sortez. Les deux sont des façons de dire « exécute mon code, mais enveloppe quelque chose de garanti autour ». Une fois que vous voyez cette forme commune, le mystérieux @ et le quotidien with open(...) cessent d’être magiques.

Cette leçon construit chacun à partir de zéro, dans l’ordre où ça fait tilt. D’abord ce qu’est réellement un décorateur — la fermeture wrapper, et pourquoi @deco n’est que f = deco(f). Puis functools.wraps, l’habitude de décorateur la plus importante (sautez-la et votre fonction oublie son propre nom). Puis les décorateurs à arguments — le motif fabrique qui fait trébucher tout le monde, rendu concret. Puis les gestionnaires de contexte : le motif de ressource, d’abord comme une classe (__enter__/__exit__), puis le style générateur plus simple @contextlib.contextmanager, plus contextlib.suppress. Nous terminons en les mettant côte à côte pour que vous sachiez lequel choisir. Chaque résultat ci-dessous a été produit par le code montré, et les blocs s’enchaînent dans l’ordre. Pour expérimenter, modifiez la cellule Essayez en direct vers la fin et appuyez sur Run.

Ce qu’est un décorateur

Retirez le @ et un décorateur n’a rien de remarquable : c’est une fonction qui prend une fonction comme argument et renvoie une nouvelle fonction. La nouvelle fonction appelle généralement l’original, mais fait quelque chose en plus autour. En voici une écrite à la main, appliquée à la main — pas de @ où que ce soit pour l’instant :

def announce(fn):                     # a decorator TAKES a function...
    def wrapper(*args, **kwargs):     # ...and returns a NEW function that wraps it
        print(f"-> calling {fn.__name__}")
        result = fn(*args, **kwargs)  # run the original, unchanged
        print(f"<- {fn.__name__} returned {result}")
        return result
    return wrapper                    # hand back the wrapper, NOT its result

def add(a, b):
    return a + b

add = announce(add)                   # wrap it by hand — this is ALL @announce does
print(add(2, 3))
-> calling add
<- add returned 5
5

Voyons ce qui s’est passé. announce(add) a renvoyé wrapper — une toute nouvelle fonction qui capture fn (l’add original). Cette astuce du « se souvenir de la variable fn » est une fermeture (closure) : wrapper garde une référence vivante vers fn même après le retour d’announce. Nous avons réassigné le nom add à ce wrapper, si bien qu’appeler add(2, 3) exécute désormais le wrapper — qui affiche -> calling add, appelle la vraie fonction pour obtenir 5, affiche <- add returned 5, et renvoie le 5. Le comportement supplémentaire enveloppe l’original ; l’original n’a jamais changé.

La ligne @ n’est rien de plus qu’un raccourci pour cette réassignation. Ces deux-ci sont identiques :

@announce                             # sugar for: multiply = announce(multiply)
def multiply(a, b):
    return a * b

print(multiply(4, 5))
-> calling multiply
<- multiply returned 20
20

@announce au-dessus de def multiply fait exactement ce que multiply = announce(multiply) faisait pour add — c’est la même enveloppe, juste écrite plus proprement. Vient maintenant le vrai gain : un décorateur de chronométrage que vous pouvez poser sur n’importe quelle fonction pour mesurer sa durée.

import time

def timer(fn):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)                       # the real work
        print(f"{fn.__name__} ran in {time.perf_counter() - start:.1f} s")
        return result
    return wrapper

@timer
def slow_add(a, b):
    time.sleep(0.2)                   # stand in for slow work / a slow call
    return a + b

print(slow_add(3, 4))
slow_add ran in 0.2 s
7

slow_add rapporte maintenant sa propre durée d’exécution — 0.2 s, le time.sleep(0.2) tenant lieu de vrai travail — et renvoie toujours 7. Écrivez timer une fois et vous pouvez poser @timer sur une centaine de fonctions ; cette réutilisation à travers les fonctions est la raison d’être des décorateurs. Il y a pourtant un bug tapi dans cette version, et il mord dès que vous faites de l’introspection sur la fonction décorée.

functools.wraps : préserver le nom

Demandez à la fonction chronométrée ce qu’elle est, et elle vous ment. Parce que nous avons remplacé slow_add par wrapper, la fonction décorée rapporte désormais l’identité du wrapper, pas la sienne :

import functools

def timed(fn):                        # same shape as `timer`, still no wraps
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@timed
def work(n):
    "sum of squares"
    return sum(i * i for i in range(n))

print("result: ", work(1000))
print("name:   ", work.__name__)      # NOT 'work'
print("doc:    ", work.__doc__)        # the docstring is gone
result:  332833500
name:    wrapper
doc:     None

La fonction fonctionne toujours — work(1000) calcule 332833500 — mais work.__name__ vaut maintenant wrapper et work.__doc__ vaut None. Chaque fonction décorée s’effondre dans le même wrapper anonyme : help(work) est inutile, une trace d’exécution nomme wrapper, et les outils qui lisent __name__ (journalisation, registres, functools.singledispatch) se comportent mal. La correction tient en une ligne — @functools.wraps(fn) sur le wrapper, qui copie l’identité de l’original :

def timed(fn):
    @functools.wraps(fn)               # copy __name__, __doc__, __module__, __wrapped__...
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@timed
def work(n):
    "sum of squares"
    return sum(i * i for i in range(n))

print("result:  ", work(1000))
print("name:    ", work.__name__)
print("doc:     ", work.__doc__)
print("unwrap:  ", work.__wrapped__.__name__)
result:   332833500
name:     work
doc:      sum of squares
unwrap:   work

Maintenant la fonction décorée conserve sa propre identité : __name__ vaut work, __doc__ vaut sum of squares, et le résultat est inchangé à 332833500. functools.wraps définit aussi __wrapped__ — une poignée directe sur la fonction originale, non décorée (son nom est toujours work) — pour que vous puissiez atteindre la fonction à travers le décorateur au besoin. Faites de @functools.wraps(fn) un réflexe sur chaque décorateur que vous écrivez ; ça coûte une ligne et vous épargne toute une classe de débogages déroutants.

Les décorateurs à arguments

@timer ne prend aucun argument — mais @repeat(3) et @retry(times=3), eux, en prennent clairement. Ajouter un argument ajoute toute une couche : il vous faut une fonction qui renvoie un décorateur. Un décorateur paramétré est donc trois fonctions imbriquées, chacune prenant une chose différente :

  1. la fabrique — prend l’argument (3) et renvoie…
  2. le décorateur — prend la fonction et renvoie…
  3. le wrapper — prend les arguments d’appel et fait le travail.

Voici repeat, qui appelle la fonction enveloppée n fois et rassemble les résultats :

def repeat(n):                        # (1) factory: takes the ARGUMENT
    def decorator(fn):                # (2) decorator: takes the FUNCTION
        @functools.wraps(fn)
        def wrapper(*args, **kwargs): # (3) wrapper: takes the CALL args
            return [fn(*args, **kwargs) for _ in range(n)]
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hi {name}"

print(greet("Ada"))
['Hi Ada', 'Hi Ada', 'Hi Ada']

greet("Ada") renvoie ['Hi Ada', 'Hi Ada', 'Hi Ada'] — trois appels, rassemblés dans une liste. Suivez la ligne @repeat(3) pour comprendre pourquoi trois couches sont nécessaires : repeat(3) s’exécute en premier et renvoie decorator (l’argument 3 est maintenant capturé dans une fermeture) ; puis decorator est appliqué à greet et renvoie wrapper ; et wrapper est ce que greet finit par devenir. Ainsi @repeat(3) est un raccourci pour greet = repeat(3)(greet) — deux appels, d’où deux fonctions internes plus la fabrique. Oubliez une couche (écrivez deux fonctions au lieu de trois) et vous obtenez l’erreur classique : @repeat(3) essaie d’utiliser 3 comme fonction à décorer, et Python se plaint qu’un int n’est pas appelable. Quand un décorateur prend des arguments, comptez les parenthèses : @repeat(3) comporte un appel, il lui faut donc la couche fabrique supplémentaire.

Les gestionnaires de contexte : l’instruction with

Passons à l’autre outil « d’enveloppe ». Un gestionnaire de contexte garantit une paire d’actions autour d’un bloc : mise en place à l’entrée, nettoyage à la sortie — et le nettoyage s’exécute quelle que soit la manière dont le bloc se termine, y compris sur une exception. Vous en utilisez déjà un chaque fois que vous écrivez with open(...) as f: — le fichier est fermé pour vous, que le bloc se termine normalement ou plante. Vous pouvez écrire le vôtre en implémentant deux méthodes : __enter__ (s’exécute au with, sa valeur de retour est reliée à la variable as) et __exit__ (s’exécute quand le bloc se termine, toujours).

import time

class timer_block:
    """Time a block of code with `with`."""
    def __init__(self, label):
        self.label = label

    def __enter__(self):                             # setup: runs at `with`
        print(f"[{self.label}] start")
        self.start = time.perf_counter()
        return self                                  # bound to the `as` variable

    def __exit__(self, exc_type, exc_val, exc_tb):   # teardown: ALWAYS runs
        print(f"[{self.label}] done in {time.perf_counter() - self.start:.1f} s")
        return False                                 # False -> propagate any exception

with timer_block("load"):
    time.sleep(0.1)
[load] start
[load] done in 0.1 s

__enter__ a affiché [load] start et a lancé le chronomètre ; le bloc a dormi ; puis __exit__ s’est déclenché automatiquement à la fin du with et a affiché [load] done in 0.1 s. L’intérêt, c’est le nettoyage garanti — et la garantie tient quand le bloc lève une exception :

try:
    with timer_block("risky"):
        raise ValueError("boom")     # __exit__ STILL runs, then the error propagates
except ValueError as e:
    print("caught outside the block:", e)
[risky] start
[risky] done in 0.0 s
caught outside the block: boom

Même si le bloc a levé une exception immédiatement, __exit__ s’est quand même exécuté et a affiché [risky] done in 0.0 s — le nettoyage n’est pas sauté par une erreur, ce qui est tout l’intérêt d’un gestionnaire de contexte. Les trois arguments que reçoit __exit__ (exc_type, exc_val, exc_tb) décrivent l’exception, ou valent tous None en cas de sortie propre. Sa valeur de retour décide du sort de l’exception : renvoyez False (ou None) pour laisser l’exception se propager — c’est pourquoi nous avons attrapé ValueError à l’extérieur du bloc — et renvoyez True pour l’avaler. Avaler en silence est presque toujours un bug (voir Problèmes fréquents), donc False est le bon défaut.

La façon plus simple : @contextlib.contextmanager

Écrire une classe avec deux méthodes dunder, c’est beaucoup de cérémonie pour une simple garde. Le décorateur @contextlib.contextmanager vous laisse écrire un gestionnaire de contexte comme un générateur à la place : tout ce qui est avant l’unique yield est la mise en place, le yield passe la main (et une valeur optionnelle) au bloc, et tout ce qui est après le yield est le nettoyage. Enveloppez le yield dans try/finally pour que le nettoyage s’exécute même en cas d’erreur :

import contextlib

@contextlib.contextmanager
def managed(name):
    print(f"open {name}")            # setup: everything BEFORE yield
    resource = {"name": name}
    try:
        yield resource               # hand the resource to the block (the `as` value)
    finally:
        print(f"close {name}")       # teardown: everything AFTER yield, even on error

with managed("db") as res:
    print("using", res["name"])
open db
using db
close db

La sortie se lit de haut en bas exactement comme le flux s’exécute : open db (mise en place), using db (le bloc, qui a reçu la ressource yieldée), close db (nettoyage). Le try/finally est ce qui la rend robuste : si le bloc avait levé une exception, Python renverrait l’exception à l’intérieur, au niveau du yield, et le finally exécuterait quand même close db avant que l’erreur ne se propage. Ce style générateur est celui que la plupart des gens choisissent — c’est la même garantie de mise en place/nettoyage que la classe, en trois fois moins de code.

Pour le cas restreint « ignore simplement cette erreur précise », la bibliothèque standard fournit déjà un gestionnaire de contexte — contextlib.suppress :

with contextlib.suppress(ZeroDivisionError):
    result = 1 / 0                   # raised...
    print("never reached")           # ...so this line is skipped
print("survived — ZeroDivisionError was suppressed")
survived — ZeroDivisionError was suppressed

suppress(ZeroDivisionError) a avalé la ZeroDivisionError, donc le bloc s’est arrêté à la division mais le programme a continué — en affichant survived — ZeroDivisionError was suppressed. C’est un remplacement propre et explicite pour un try/except SomeError: pass quand vous voulez réellement ignorer une exception précise.

Décorateurs et gestionnaires de contexte ensemble

Les deux outils partagent un modèle mental, et contextlib rend le recouvrement littéral : une fonction @contextlib.contextmanager produit un objet qui peut servir soit de gestionnaire de contexte (autour d’un bloc) soit de décorateur (autour d’une fonction entière). Le même section ci-dessous fait les deux :

@contextlib.contextmanager
def section(title):
    print(f"== {title} ==")
    try:
        yield
    finally:
        print(f"== /{title} ==")

with section("report"):              # used as a CONTEXT MANAGER, around a block
    print("  ...building the report...")

@section("job")                      # the SAME object used as a DECORATOR, around a function
def run_job():
    print("  ...running the job...")

run_job()
== report ==
  ...building the report...
== /report ==
== job ==
  ...running the job...
== /job ==

Les deux écritures enveloppent leur cible dans les mêmes marqueurs == … == / == /… == : le with enveloppe un bloc, le @section("job") enveloppe tout le corps de fonction de run_job. C’est un raccourci pratique, mais il ne brouille pas le choix de l’outil à privilégier. La distinction tient à la portée de la réutilisation :

Décorateur Gestionnaire de contexte
Enveloppe une fonction entière un bloc de code (with …:)
À privilégier quand vous voulez le même comportement sur plusieurs fonctions (chronométrage, journalisation, réessai, cache, authentification) vous devez garantir mise en place + nettoyage autour de quelques instructions (ouvrir/fermer, acquérir/libérer, démarrer/arrêter)
À écrire comme une fonction renvoyant un wrapper décoré de @functools.wraps une classe (__enter__/__exit__) ou un générateur @contextmanager
La garantie exécute votre code supplémentaire autour de chaque appel à la fonction exécute le nettoyage à chaque sortie du bloc, même en cas d’erreur

Règle empirique : décorateur = réutiliser un comportement à travers des fonctions ; gestionnaire de contexte = garantir mise en place/nettoyage autour d’un bloc. Si vous vous surprenez à coller le même try/finally dans plusieurs fonctions, c’est un décorateur ; si vous avez besoin qu’une ressource soit nettoyée autour de quelques instructions précises, c’est un with.

Essayez en direct

Les blocs ci-dessus se sont exécutés au moment du build. Modifiez celui-ci et appuyez sur Run pour écrire votre propre décorateur et votre propre gestionnaire de contexte — il s’exécute dans votre navigateur via Pyodide (aucune installation). Les décorateurs et gestionnaires de contexte sont du Python pur, donc cela s’exécute instantanément, sans rien à télécharger.

🟢 Avec un agent IA

Vous recopiez le même nettoyage try/finally de fonction en fonction, ou le même bloc de chronométrage/journalisation partout ? Collez votre code et demandez à Prova « transforme cette logique répétée en décorateur » — ou « enveloppe cette ressource dans un gestionnaire de contexte pour qu’elle soit toujours nettoyée, même en cas d’erreur ». Elle écrit le wrapper @functools.wraps (ou le générateur @contextmanager), préserve intacts le nom et la docstring de votre fonction, et vous montre l’avant/après. Parce que le runtime est juste là, vous l’exécutez et confirmez que la version enveloppée donne toujours le même résultat — vous ne faites pas confiance à la réécriture, vous la prouvez. The runtime is the judge. Demandez à Prova →

La même idée en R

Vous travaillez dans les deux langages ? R n’a pas de syntaxe @decorator, mais il a bien les mêmes idées sous d’autres noms. Pour « envelopper une fonction », R utilise des opérateurs de fonction — des fonctions qui prennent une fonction et en renvoient une version modifiée ; le paquet purrr du tidyverse en fournit des tout prêts comme safely(), possibly() et quietly() (un safely(f) renvoie une nouvelle fonction qui capture les erreurs au lieu de s’arrêter), ce qui est exactement un décorateur sous un autre nom. Pour l’idée du gestionnaire de contexte — nettoyage garanti autour d’un bloc — R base propose on.exit() (enregistre un nettoyage qui s’exécute quand la fonction se termine, même en cas d’erreur), et le paquet withr du tidyverse offre des utilitaires with_*() / local_*() (withr::with_options(), with_tempfile()) qui mettent quelque chose en place, exécutent votre code, puis restaurent l’état ensuite. Le concept se transpose : enveloppez une fonction pour réutiliser un comportement, et gardez un bloc pour garantir le nettoyage. Voir le pilier Programmation pour le versant R.

Problèmes fréquents

Vous avez oublié functools.wraps, et maintenant la fonction décorée a le mauvais nom. Sans @functools.wraps(fn) sur le wrapper, le __name__ de la fonction décorée devient "wrapper" et son __doc__ devient None — cassant help(), les traces d’exécution, la journalisation qui lit __name__, et des outils comme functools.singledispatch. Ce n’est pas cosmétique ; ça corrompt silencieusement l’introspection. Mettez @functools.wraps(fn) sur chaque wrapper que vous écrivez, toujours.

Un décorateur à arguments a besoin de trois fonctions imbriquées — vous en avez écrit deux. @repeat(3) c’est repeat(3)(greet) : repeat(3) doit renvoyer un décorateur, qui prend ensuite la fonction. Si vous n’écrivez que factory → wrapper (deux couches), @repeat(3) essaie de décorer l’entier 3 et vous obtenez TypeError: 'int' object is not callable (ou la fonction finit passée comme argument). Comptez les couches : fabrique (prend l’argument) → décorateur (prend la fonction) → wrapper (prend l’appel). Les décorateurs sans argument n’ont que les deux dernières.

Le __exit__ d’un gestionnaire de contexte renvoie True et avale les exceptions en silence. Si __exit__ renvoie une valeur vraie, Python traite l’exception comme gérée et l’exécution continue au-delà du with comme si de rien n’était — masquant de vrais bugs. Renvoyez False (ou None, le défaut) pour que les exceptions se propagent ; ne renvoyez True que lorsque vous voulez délibérément « supprimer ceci », et préférez l’explicite contextlib.suppress(SomeError) pour cela.

Votre générateur @contextmanager saute son nettoyage en cas d’erreur — pas de try/finally. Avec @contextlib.contextmanager, le code après yield est le nettoyage. Si le bloc with lève une exception, Python renvoie l’exception à l’intérieur au niveau du yield — donc tout nettoyage écrit simplement après yield est sauté. Enveloppez le yield dans try/finally (le nettoyage dans le finally) pour que le nettoyage s’exécute aussi bien sur le chemin heureux que sur le chemin d’erreur. Aussi : le générateur doit yield exactement une fois — zéro ou deux yields lève une RuntimeError.

Questions fréquentes

functools.wraps est un décorateur que vous appliquez à votre fonction wrapper@functools.wraps(fn) — qui copie l’identité de la fonction enveloppée (__name__, __doc__, __module__, __qualname__, et le dictionnaire d’annotations) sur le wrapper, et définit __wrapped__ pour pointer vers l’original. Sans lui, chaque fonction que vous décorez rapporte le nom du wrapper ("wrapper") et perd sa docstring, ce qui casse help(), rend les traces d’exécution déroutantes, et trompe tout outil qui inspecte __name__. C’est une ligne et cela rend vos décorateurs transparents, alors traitez-le comme obligatoire sur chaque décorateur.

Ajoutez une couche d’imbrication de plus. Un décorateur à arguments est une fabrique qui prend les arguments et renvoie un décorateur ordinaire, lequel prend la fonction et renvoie le wrapper — trois fonctions au total. Par exemple, def repeat(n): def decorator(fn): @functools.wraps(fn) def wrapper(*a, **k): return [fn(*a, **k) for _ in range(n)]; return wrapper; return decorator. Alors @repeat(3) signifie greet = repeat(3)(greet) : repeat(3) s’exécute en premier et renvoie decorator, qui enveloppe ensuite greet. L’erreur classique est de n’écrire que deux couches, ce qui fait que @repeat(3) essaie de décorer le nombre 3.

Ce sont deux parties de la même chose. Le décorateur est la fonction externe que vous appliquez avec @ — elle prend une fonction et renvoie un remplaçant. Le wrapper est la fonction interne que le décorateur renvoie et rend — le remplaçant effectif qui s’exécute à chaque appel de la fonction décorée, en ajoutant du comportement autour d’un appel à l’original. Ainsi @timer est le décorateur ; le wrapper qu’il définit est ce que votre fonction devient. Vous écrivez le décorateur ; le wrapper est la machinerie qu’il produit (et ce dont functools.wraps corrige l’identité).

De deux façons. Comme une classe, définissez __enter__ (mise en place ; sa valeur de retour est reliée à la variable as) et __exit__(self, exc_type, exc_val, exc_tb) (nettoyage ; s’exécute à chaque sortie, renvoie False pour laisser les exceptions se propager). Comme un générateur, décorez une fonction avec @contextlib.contextmanager et faites yield une fois : le code avant le yield est la mise en place, le code après est le nettoyage, et vous enveloppez le yield dans try/finally pour que le nettoyage s’exécute même en cas d’erreur. Le style générateur est généralement plus court et c’est celui que la plupart des gens choisissent ; utilisez la classe quand vous devez stocker un état plus riche ou réutiliser l’objet.

Ils produisent le même comportement, choisissez donc selon le style et les besoins. Utilisez @contextlib.contextmanager (le style générateur) dans la plupart des cas — c’est plus court, ça se lit de haut en bas comme mise en place → yield → nettoyage, et c’est facile à réussir avec un try/finally. Utilisez une classe avec __enter__/__exit__ quand le gestionnaire de contexte doit détenir un état non trivial, exposer des méthodes sur l’objet relié à as, être réutilisé comme une instance à longue durée de vie, ou gérer les exceptions avec une logique personnalisée dans __exit__. Les deux garantissent le nettoyage à chaque sortie ; la classe vous donne simplement un objet plus riche quand vous en avez besoin.

Testez vos connaissances

Écrivez un décorateur @count_calls qui enregistre combien de fois la fonction décorée a été appelée, en exposant le total comme un attribut sur la fonction.

Tâche 1. Écrivez count_calls(fn) qui renvoie un wrapper incrémentant un compteur à chaque exécution, et stocke ce compteur dans wrapper.calls (initialisez-le à 0 avant de renvoyer). Utilisez @functools.wraps(fn) pour que la fonction décorée conserve son propre nom. Décorez une petite fonction, appelez-la quelques fois, et affichez le compte.

Tâche 2. En une phrase, expliquez ce qui tournerait mal si vous omettiez @functools.wraps(fn).

Une fonction est un objet, vous pouvez donc y accrocher un attribut : définissez wrapper, puis après le def écrivez wrapper.calls = 0, et à l’intérieur de wrapper faites wrapper.calls += 1 avant d’appeler fn. Renvoyez wrapper. Lisez ensuite le total avec ping.calls — l’attribut vit sur le wrapper, qui est la fonction décorée.

import functools

def count_calls(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1            # bump the counter each call
        return fn(*args, **kwargs)
    wrapper.calls = 0                 # start the counter before returning
    return wrapper

@count_calls
def ping():
    return "pong"

ping(); ping(); ping()
print(ping.calls)        # 3
print(ping.__name__)     # 'ping'  <- kept by functools.wraps

ping.calls affiche 3 — trois appels, trois incréments — et ping.__name__ est toujours 'ping'. Tâche 2 : sans @functools.wraps(fn), ping.__name__ rapporterait "wrapper" et ping.__doc__ vaudrait None, cassant help(), les traces d’exécution, et tout code qui identifie la fonction par son nom.

Vérification rapide. La méthode __exit__ d’un gestionnaire de contexte renvoie True. À l’intérieur du bloc with, une exception est levée. Que se passe-t-il ?

L’exception est avalée en silence : renvoyer une valeur vraie depuis __exit__ dit à Python que l’exception a été gérée, si bien que l’exécution continue au-delà du bloc with comme si de rien n’était. C’est presque toujours un bug — une vraie erreur disparaît sans laisser de trace. Renvoyez False (ou None, le défaut) pour que les exceptions se propagent, et recourez à contextlib.suppress(SomeError) quand vous voulez délibérément ignorer une exception précise.

Conclusion

Vous savez maintenant écrire les deux outils « d’enveloppe » à partir de zéro. Un décorateur est une fonction qui prend une fonction et en renvoie une nouvelle ; @deco n’est que f = deco(f), et @functools.wraps(fn) sur le wrapper préserve intacts le nom et la docstring de la fonction décorée — une habitude obligatoire. Un décorateur à arguments, c’est trois fonctions imbriquées (fabrique → décorateur → wrapper). Un gestionnaire de contexte garantit la mise en place et le nettoyage autour d’un bloc même en cas d’erreur : écrivez-le comme une classe avec __enter__/__exit__, ou plus simplement avec @contextlib.contextmanager (mise en place avant yield, nettoyage après, enveloppé dans try/finally), avec contextlib.suppress pour le cas « ignorer une erreur ». Lequel choisir se résume à une ligne : le décorateur pour réutiliser un comportement à travers des fonctions, le gestionnaire de contexte pour garantir mise en place/nettoyage autour d’un bloc.

Leçons connexes

  • Poursuivez avec : Itérateurs et générateurs Python — une fonction @contextlib.contextmanager est un générateur (mise en place, yield, nettoyage), donc le yield que vous y avez appris est le même qu’ici · Compréhensions Python — les fermetures et les motifs *args/**kwargs sur lesquels s’appuie un wrapper.
  • Allez plus loin : les leçons suivantes de cette série Python Intermédiaire s’appuient sur ces motifs — la programmation orientée objet (où __enter__/__exit__ ne sont que deux des nombreuses méthodes dunder), la programmation asynchrone (async with et les gestionnaires de contexte asynchrones). Voir le pilier Programmation pour l’ensemble complet des leçons R et Python.
Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Python intermédiaire — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Décorateurs et gestionnaires de contexte en Python},
  date = {2026-07-18},
  url = {https://www.datanovia.com/learn/programming/python-intermediate/decorators-context-managers},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Décorateurs et gestionnaires de contexte en Python.” 2026. July 18. https://www.datanovia.com/learn/programming/python-intermediate/decorators-context-managers.