Avatar

Gilles Fabio

Developer

Python et les décorateurs

Introduction

Vous débutez en programmation Python ? Python vous intéresse ? Vous avez certainement déjà entendu parler des “décorateurs”. C’est une fonctionnalité très utilisée dans le monde Python. Ils permettent d’écrire du code concis, lisible et non-répétitif.

Concrètement, un décorateur est un callable qui prend pour argument un callable et qui retourne une copie de ce même callable en le “décorant”, c’est-à-dire en effectuant un pré-traitement et/ou un post-traitement sur celui-ci. Dans le jargon, on parle souvent de wrapping (on enveloppe, on saupoudre, on apporte un “plus”, on décore).

Si vous connaissez un peu le framework web Django, vous avez peut-être déjà manipulé des décorateurs, notamment pour la gestion de l’authentification. Pour protéger les vues, il suffit de les décorer avec les décorateurs @login_required ou @permission_required.

Un exemple :

@login_required
def protected_view(request):
    ...

Dans cet exemple, la vue protected_view est automatiquement protégée. Si vous n’êtes pas un utilisateur enregistré et connecté, vous serez redirigé vers le formulaire d’authentification, puisque le décorateur @login_required a ajouté son grain de sel pour que nous écrivions le moins de code possible.

Le micro-framework web Bottle utilise essentiellement cette fonctionnalité du langage.

Un exemple :

@route('/hello/:names')
@view('hello')
def hello(names):
   names = names.split(',')
   return dict(title='Hello World', names=names)

Ici, le décorateur @route se charge de définir une route pour cette vue et le décorateur @view se charge de charger le template hello.tpl et d’y injecter les variables retournées dans son contexte. Comme vous pouvez le constater, nous pouvons exécuter plusieurs décorateurs sur une même fonction car nous pouvons les “chaîner”.

Il existe deux types de syntaxes.

La syntaxe arobase @ :

@dec2
@dec1
def func(arg1, arg2):
    pass

Et la syntaxe classique :

def func(arg1, arg2):
    pass

func = dec2(dec1(func))

Les deux exemples de code ci-dessus sont équivalents. On utilise, généralement, la syntaxe @ lorsque nous souhaitons “figer” le comportement de la fonction (lors de son appel, ses décorateurs seront alors systématiquement exécutés) et la syntaxe classique si nous n’avons pas accés à sa définition ou lorsqu’on souhaite modifier son comportement à un endroit précis dans le code.

Les décorateurs peuvent prendre des arguments requis ou optionnels, en plus de la fonction à décorer. On peut alors modifier dynamiquement leur comportement. C’est le cas dans l’exemple du framework web Bottle avec les décorateurs @route et @view. C’est aussi le cas pour les décorateurs @login_required ou @permission_required de Django.

Un exemple avec @permission_required:

@permission_required('polls.can_vote', login_url='/loginpage/')
def my_view(request):
    ...

Non seulement de prendre pour argument la fonction à décorer, ce décorateur prend aussi un argument requis (la permission) et un argument optionnel (l’URL du formulaire d’authentification en cas d’échec).

Maintenant que nous connaissons un peu mieux les décorateurs, passons à la pratique. Nous allons, tout d’abord, implémenter un décorateur “simple” ne prenant pas d’argument. Nous aborderons ensuite l’implémentation de décorateurs plus élaborés, capables d’accepter des arguments (requis et/ou optionnels). Nous ferons ensuite connaissance avec functools.wraps (inclus dans Python). Et nous terminerons avec un exemple concret.

Décorateur simple

Commençons par un premier exemple :

# -*- coding: utf-8 -*-

# Notre décorateur
def decorate(func):
    print u"Je suis dans la fonction 'decorate' et je décore '%s'." % func.__name__
    print u"Exécution de la fonction '%s'." % func.__name__
    return func

# Notre fonction décorée
@decorate
def foobar(*args):
    print ", ".join(args)

# Appel de la fonction
foobar("A", "B", "C", "D")

Exécutons le code :

Je suis dans la fonction 'decorate' et je décore 'foobar'.
Exécution de la fonction 'foobar'.
A, B, C, D

Notre fonction a bien été décorée. Mais dans cet exemple, nous avons pas accès aux arguments de la fonction décorée. On affiche simplement une chaîne de caractères pour s’informer de l’éxecution du décorateur.

Modifions notre code pour pouvoir accéder aux arguments de la fonction décorée :

# -*- coding: utf-8 -*-

# Notre décorateur
def decorate(func):
    print u"Je suis dans la fonction 'decorate' et je décore '%s.'" % func.__name__
    def wrapper(*args, **kwargs):
        print u"Je suis dans la fonction 'wrapper' qui accède aux arguments de '%s'." % func.__name__
        a = list(args)
        a.reverse()
        print u"Je t'en donne la preuve, je peux les inverser : %s." % ', '.join(a)
        print u"Exécution de la fonction '%s'." % func.__name__
        return func(*args, **kwargs)
    return wrapper

# Notre fonction décorée
@decorate
def foobar(*args):
    print ", ".join(args)

# Appel de la fonction
foobar("A", "B", "C", "D")

Exécutons-le :

Je suis dans la fonction 'decorate' et je décore 'foobar.'
Je suis dans la fonction 'wrapper' qui accède aux arguments de 'foobar'.
Je t'en donne la preuve, je peux les inverser : D, C, B, A.
Exécution de la fonction 'foobar'.
A, B, C, D

Nous avons créé un wrapper, qui n’est autre qu’une fonction possédant la même signature (les mêmes arguments) que la fonction à décorer et qui retourne la fonction décorée, avec ses arguments. Le wrapper est lui-même retourné par le décorateur. C’est un système à la poupées Russes.

Dans cet exemple, nous avons seulement effectué un pré-traitement. Mais il est tout à fait possible, très simplement, d’effectuer un post-traitement sur la fonction à décorer :

# -*- coding: utf-8 -*-

# Notre décorateur
def decorate(func):
    print u"Je suis dans la fonction 'decorate' et je décore '%s.'" % func.__name__
    def wrapper(*args, **kwargs):
        print u"Je suis dans la fonction 'wrapper' qui accède aux arguments de '%s'." % func.__name__
        a = list(args)
        a.reverse()
        print u"J'en donne la preuve, je peux les inverser : %s." % ', '.join(a)
        print u"Exécution de la fonction '%s'." % func.__name__
        response = func(*args)
        print u"Je peux effectuer, ici, un post-traitement."
        return response
    return wrapper

# Notre fonction décorée
@decorate
def foobar(*args):
    print ", ".join(args)

# Appel de la fonction
foobar("A", "B", "C", "D")

Exécutons le code :

Je suis dans la fonction 'decorate' et je décore 'foobar.'
Je suis dans la fonction 'wrapper' qui accède aux arguments de 'foobar'.
Je t'en donne la preuve, je peux les inverser : D, C, B, A.
Exécution de la fonction 'foobar'.
A, B, C, D
Je peux effectuer, ici, un post-traitement.

Pour simplifier au maximum :

def decorate(func):
    def wrapper(*args, **kwargs):
        # Pré-traitement
        response = func(*args, **kwargs)
        # Post-traitement
        return response
    return wrapper

Ici, je récupère le retour de la fonction dans response et je retourne ensuite la valeur après le post-traitement. Mais si vous n’avez pas besoin d’accéder aux données retournées par la fonction à décorer, ce qui était un peu notre cas, il suffit juste d’appeller la fonction :

def decorate(func):
    def wrapper(*args, **kwargs):
        # Pré-traitement
        func(*args, **kwargs)
        # Post-traitement
    return wrapper

Cela dépend vraiment de votre implémentation.

C’est un peu plus clair ? Si vous avez compris les exemples ci-dessus et ce bout de code, vous avez tout compris.

J’entends certains penser fortement… Si un décorateur prend pour argument un callable, on doit pouvoir lui passer d’autres arguments, non ? Si, par exemple, on souhaite modifier son comportement dynamiquement ? Comme pour le décorateur @permission_required de Django, qui prend pour arguments une permission donnée et, éventuellement, l’URL de la page de connexion pour rediriger automatiquement l’utilisateur en cas d’échec :

@permission_required('polls.can_vote', login_url='/loginpage/')
def my_view(request):
    ...

Ou bien, comme avec les vues de Bottle et leurs décorateurs @route et @view:

@route('/hello/:names')
@view('hello')
def hello(names):
   names = names.split(',')
   return dict(title='Hello World', names=names)

Bien vu. Nous avons abordé l’implémentation d’un décorateur simple qui ne prend pas d’argument, excepté le callable à décorer. On ne peut donc pas modifier dynamiquement son comportement en précisant diverses options. Voyons comment implémenter un décorateur capable d’accepter des arguments.

Décorateur avec arguments

Pour implémenter un décorateur dynamique capable d’accépter des arguments (ou “options”), nous devons ajouter un niveau supplémentaire : un décorateur qui prend pour arguments les arguments du décorateur de la fonction à décorer. Vous suivez toujours ? Pour un décorateur simple, nous avons deux niveaux de fonctions (le décorateur et le wrapper). Pour un décorateur avec arguments, nous en avons trois (le décorateur du décorateur, le décorateur et le wrapper).

Si on simplifie au maxium :

def decorate(arg1, arg2, arg3):
    def decorated(func):
        def wrapper(*args, **kwargs):
            # Pré-traitement
            response = func(*args, **kwargs)
            # Post-traitement
            return response
        return wrapper
    return decorated

Prenons un exemple :

# -*- coding: utf-8 -*-

def decorate(arg1, arg2, arg3):
    print u'Je suis dans la fonction "decorate".'
    def decorated(func):
        print u'Je suis dans la fonction "decorated".'
        def wrapper(*args, **kwargs):
            print u'Je suis dans la fonction "wrapper".'
            print u"Les arguments du décorateurs sont : %s, %s, %s." % (arg1, arg2, arg3)
            print u"Pré-traitement."
            print u"Exécution de la fonction %s." % func.__name__
            response = func(*args, **kwargs)
            print u"Post-traitement."
            return response
        return wrapper
    return decorated

@decorate("Arg 1", "Arg 2", "Arg 3")
def foobar():
    print u"Je suis foobar, je vous reçois 5 sur 5."

foobar()

Si on exécute le code :

Je suis dans la fonction "decorate".
Je suis dans la fonction "decorated".
Je suis dans la fonction "wrapper".
Les arguments du décorateurs sont : Arg 1, Arg 2, Arg 3.
Pré-traitement.
Exécution de la fonction foobar.
Je suis foobar, je vous reçois 5 sur 5.
Post-traitement.

Les arguments passés au décorateur ont bien été pris en compte.

Nous avons implémenté un décorateur avec arguments obligatoires (ou requis). Pour passer des arguments optionnels :

def decorate(arg1='default', arg2=None, arg3=None):
    def decorated(func):
        def wrapper(*args, **kwargs):
            # Pré-traitement
            response = func(*args, **kwargs)
            # Post-traitement
            return response
        return wrapper
    return decorated

@decorate(arg1='my value')
def foobar():
    pass

Attention. Comme tous les arguments de ce décorateur sont optionnels, si on ne souhaite pas les modifier, on serait tenté d’appeller le décorateur comme un décorateur “simple” :

@decorate
def foobar():
    pass

Si on exécute ce code :

Traceback (most recent call last):
  File "exemple.py", line 22, in <module>
    foobar()
TypeError: decorated() takes exactly 1 argument (0 given)

Catastrophe ! Python vous demandera quand même de founir un argument, qu’il considère comme “requis” (même si tous les arguments sont optionnels). Dans ce cas, la bonne syntaxe est d’utiliser les parenthèses d’appel de fonction :

@decorate()
def foobar():
    pass

Ce n’est pas très sexy, source d’erreur, mais ça fonctionne. Nous aborderons plus loin comment contourner ce “problème”.

Un autre exemple, avec un argument obligatoire et des arguments optionnels :

def decorate(arg1, arg2='default value', arg3=None):
    def decorated(func):
        def wrapper(*args, **kwargs):
            # Pré-traitement
            response = func(*args, **kwargs)
            # Post-traitement
            return response
        return wrapper
    return decorated

@decorate('required value')
def foo():
    pass

@decorate('required value', arg3='my value')
def bar():
    pass

Retenez surtout les trois niveaux de fonctions (la fonction qui prend pour arguments les arguments du décorateur, le décorateur et le wrapper) et les parenthèses d’appel de fonction pour un décorateur avec arguments optionnels appelé sans argument.

Préservation de la fonction décorée

Jusque là, on s’est intéressé au décorateur, pas vraiment à la fonction décorée.

Penchons-nous de plus près sur la fonction décorée :

# -*- coding: utf-8 -*-

def logged(func):
    def wrapper(*args, **kwargs):
        """Je suis la documentation de logged."""
        print u"Exécution de %s." % func.__name__
        return func(*args, **kwargs)
    return wrapper

@logged
def foobar(arg):
    """Je suis la documentation de foobar."""
    print arg

foobar("Yeah!")

print foobar.__name__
print foobar.__doc__

Si on exécute ce bout de code :

Exécution de foobar.
Yeah!
_logged
Je suis la documentation de logged.

Mince ! On appelle la fonction foobar et c’est pourtant le nom et la documentation du décorateur logged que nous renvoie foobar.__name__ et foobar.__doc__. Ça, c’est bien le genre de truc à donner des migraines. Si on y réfléchie bien, c’est un comportement logique puisque la fonction retournée par le décorateur est une fonction de substitution, pas notre vraie foobar.

Heureusement, si vous utilisez Python 2.5 ou supérieure, functools.wraps vient à notre rescousse. Ce décorateur va préserver notre fonction décorée avec élégance et délicatesse. Pour ce faire, nous devons juste décorer le wrapper avec ce décorateur, en lui passant comme argument la fonction à décorer. Ensuite, il se charge du reste.

Un exemple :

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

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """Je suis la documentation de logged."""
        print u"Exécution de %s." % func.__name__
        return func(*args, **kwargs)
    return wrapper

def foobar(arg):
    """Je suis la documentation de foobar."""
    print arg

foobar = logged(foobar)
foobar("Yeah!")

print foobar.__name__
print foobar.__doc__

Si on exécute ce code :

Exécution de foobar.
Yeah!
foobar
Je suis la documentation de foobar.

Hallelujah ! Ça marche.

Si vous n’utilisez pas Python 2.5 ou supérieur mais si vous utilisez Django et Python 2.4, vous pouvez quand même bénéficier de cette fonctionnalité via django.utils.functional.wraps.

Décorateurs avec ou sans arguments

Nous avons vu, plus haut, comment implémenter un décorateur avec arguments obligatoires ou optionnels. Nous nous sommes aperçu que si nous appellons un décorateur ne prenant que des arguments optionnels sans argument, comme un décorateur “simple”, Python renvoie une erreur. En ajoutant les parenthèses d’appel de fonction, on contourne le problème.

Exemple :

# -*- coding: utf-8 -*-

def decorate(arg1='default', arg2=None, arg3=None):
    def decorated(func):
        def wrapper(*args, **kwargs):
            # Pré-traitement
            response = func(*args, **kwargs)
            # Post-traitement
            return response
        return wrapper
    return decorated

# MAUVAIS
@decorate
def foobar():
    pass

# BON
@decorate()
def foobar():
    pass

Malheureusement, il n’existe pas de solution générique. Pas de solution idéale qui nous permettrait d’implémenter des décorateurs capables d’être appelés avec ou sans arguments, sans parenthèses d’appel de fonction. Un décorateur ne prend pas d’argument ? Pas de paranthèses. Un décorateur prend des arguments optionnels, même avec des valeurs par défaut ? Lorsqu’il est appelé seul, les parenthèses s’imposent. Pour un décorateur avec des arguments obligatoires, on ne se pose même pas la question puisque nous sommes “obligé” de lui donner des arguments.

Cependant, si il n’existe pas de solution “générique”, nous pouvons quand même modifier notre implémentation pour éviter l’emploi des parenthèses. Nous devons alors donner notre fonction à décorer comme premier argument au décorateur de notre décorateur. On l’initialisera à None. Si nous fournissons des arguments optionnels, la valeur de notre fonction sera None, on retournera alors une fonction prenant pour argument notre fonction à décorer et qui retournera notre décorateur. Si la valeur de notre fonction n’est pas None, cela sous-entend que nous appelons notre décorateur sans argument, ni parenthèses.

Pour mieux comprendre, modifions notre exemple :

# -*- coding: utf-8 -*-

def decorate(func=None, arg1=None, arg2=None, arg3=None):
    print u'Je suis dans la fonction "decorate".'
    def decorated(func):
        print u'Je suis dans la fonction "decorated".'
        def wrapper(*args, **kwargs):
            print u'Je suis dans la fonction "wrapper".'
            print u"Les arguments du décorateurs sont : %s, %s, %s." % (arg1, arg2, arg3)
            print u"Pré-traitement."
            print u"Exécution de la fonction %s." % func.__name__
            response = func(*args, **kwargs)
            print u"Post-traitement."
            return response
        return wrapper
    if func is None:
        # Le décorateur est appellé avec des arguments
        def decorator(func):
            return decorated(func)
        return decorator
    # Le décorateur est appellé sans arguments
    return decorated(func)

@decorate
def foobar():
    print u"Je suis foobar, je vous reçois 5 sur 5."

foobar()

Si on exécute ce code :

Je suis dans la fonction "decorate".
Je suis dans la fonction "decorated".
Je suis dans la fonction "wrapper".
Les arguments du décorateurs sont : None, None, None.
Pré-traitement.
Exécution de la fonction foobar.
Je suis foobar, je vous reçois 5 sur 5.
Post-traitement.

Ça marche ! Nous n’avons plus besoin d’ajouter les parenthèses à notre décorateur quand il est appelé sans argument. Mais, en contre-partie, cela nous contraint à lui donner uniquement des arguments nommés :

# BON
@decorate(arg1="Python", arg2="Haskell")
def foobar():
    print u"Je suis foobar, je vous reçois 5 sur 5."

# MAUVAIS
@decorate("Python", "Haskell")
def foobar():
    print u"Je suis foobar, je vous reçois 5 sur 5."

Si on exécute le second exemple :

Je suis dans la fonction "decorate".
Je suis dans la fonction "decorated".
Je suis dans la fonction "wrapper".
Les arguments du décorateurs sont : Haskell, None, None.
Pré-traitement.
Traceback (most recent call last):
  File "exemple.py", line 24, in <module>
    @decorate("Python", "Haskell")
  File "exemple.py", line 11, in wrapper
    print u"Exécution de la fonction %s." % func.__name__
AttributeError: 'str' object has no attribute '__name__'

Ça coince. Notre chaîne de caractères “Python” a été interprétée comme la fonction à décorer, puisqu’elle a été passée comme premier argument, donc logiquement considérée comme func.

Vous vous souvenez du chapitre précédent ? Notre code ne préserve pas la fonction décorée. Rajoutons un peu de documentation et appelons foobar.__name__ et foobar.__doc__ pour s’en convaincre :

# -*- coding: utf-8 -*-

def decorate(func=None, arg1=None, arg2=None, arg3=None):
    print u'Je suis dans la fonction "decorate".'
    def decorated(func):
        print u'Je suis dans la fonction "decorated".'
        def wrapper(*args, **kwargs):
            """Je suis la documentation de decorate."""
            print u'Je suis dans la fonction "wrapper".'
            print u"Les arguments du décorateurs sont : %s, %s, %s." % (arg1, arg2, arg3)
            print u"Pré-traitement."
            print u"Exécution de la fonction %s." % func.__name__
            response = func(*args, **kwargs)
            print u"Post-traitement."
            return response
        return wrapper
    if func is None:
        # Le décorateur est appellé avec des arguments
        def decorator(func):
            return decorated(func)
        return decorator
    # Le décorateur est appellé sans arguments
    return decorated(func)

@decorate(arg1="Python", arg2="Haskell")
def foobar():
    """Je suis la documentation de foobar."""
    print u"Je suis foobar, je vous reçois 5 sur 5."

print foobar.__name__
print foobar.__doc__

Exécutons :

Je suis dans la fonction "decorate".
Je suis dans la fonction "decorated".
wrapper
Je suis la documentation de decorate.

Mauvais. Faisons appel à functools.wraps :

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

def decorate(func=None, arg1=None, arg2=None, arg3=None):
    print u'Je suis dans la fonction "decorate".'
    def decorated(func):
        print u'Je suis dans la fonction "decorated".'
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            """Je suis la documentation de decorate."""
            print u'Je suis dans la fonction "wrapper".'
            print u"Les arguments du décorateurs sont : %s, %s, %s." % (arg1, arg2, arg3)
            print u"Pré-traitement."
            print u"Exécution de la fonction %s." % func.__name__
            response = func(*args, **kwargs)
            print u"Post-traitement."
            return response
        return wrapper
    if func is None:
        # Le décorateur est appellé avec des arguments
        def decorator(func):
            return decorated(func)
        return decorator
    # Le décorateur est appellé sans arguments
    return decorated(func)

@decorate(arg1="Python", arg2="Haskell")
def foobar():
    """Je suis la documentation de foobar."""
    print u"Je suis foobar, je vous reçois 5 sur 5."

print foobar.__name__
print foobar.__doc__

Exécutons :

Je suis dans la fonction "decorate".
Je suis dans la fonction "decorated".
foobar
Je suis la documentation de foobar.

C’est beaucoup mieux.

Exemple concret avec Django

Vous connaissez la fonction render_to_response de Django ? C’est un raccourci pour rendre du contenu dans un template. Cette fonction prend pour arguments : le nom du template (requis), un dictionnaire contenant les variables/valeurs à ajouter dans le contexte du template (optionnel), une instance de Context ou RequestContext (si on souhaite bénéficier des context processors) dans laquelle sera injecté le dictionnaire (par défaut, une instance de Context) et le type MIME de la réponse (par défaut, celui défini par settings.DEFAULT_CONTENT_TYPE).

Exemple :

# -*- coding: utf-8 -*-
from django.shortcuts import render_to_response

def home(request):
    """
    Page d'accueil.
    """
    return render_to_response('home.html', {'foo': 'bar'})

Dans cet exemple, nous n’accédons pas aux context processors de Django. Pour pour avoir accéder à ces fameux context processors, nous devons plutôt utiliser une instance RequestContext :

# -*- coding: utf-8 -*-
from django.shortcuts import render_to_response
from django.template import RequestContext

def home(request):
    """
    Page d'accueil.
    """
    return render_to_response(
        'home.html',
        {"foo": "bar"},
        context_instance=RequestContext(request))

On est bien d’accord, c’est un peu verbeux. Si vous êtes un développeur Django rusé, vous utilisez certainement les vues génériques (nous allons y venir). Comme nous ne sommes pas censé savoir qu’il existe déjà une solution prête-à-l’emploi, implémentons un décorateur pour rendre ce code un peu plus concis. Nous allons le nommer render.

Implémentation :

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

from django.shortcuts import render_to_response
from django.template import RequestContext

def render(template):
    def decorated(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            """
            Raccourci du raccourci ``django.shortcuts.render_to_response``.
            """
            context = func(*args, **kwargs)
            return render_to_response(
                template,
                context,
                context_instance=RequestContext(args[0]))
        return wrapper
    return decorated

@render('home.html')
def home(request):
    """
    Page d'accueil.
    """
    return {
        'title': u"Home",
        'description': u"This is the homepage.",
        'view_name': home.__name__,
        'view_doc': home.__doc__,
        }

Nos vues n’ont plus qu’à retourner un dictionnaire des variables/valeurs à injecter dans le context du template, que nous donnons comme argument au décorateur. Notre décorateur render se charge du reste. Les variables view_name et view_doc doivent respectivement retourner le nom et la documentation de la vue, et non celles du décorateur. Merci functools.wrap.

Oui mais… On a réinventé la roue. Django propose déjà des vues génériques, dont notamment direct_to_template. Nous n’avons, d’ailleurs, même plus besoin d’écrire la moindre vue. On spécifie tout ça dans notre module urls.py.

Notre code se limite à :

urlpatterns = patterns('',
    url(r'^foobar/$', 'django.views.generic.simple.direct_to_template', {
        'template': 'foobarapp/index.html',
        'extra_context': {
            "foo": "bar",
        }},
        name="foobar",
    ),
)

Merci les vues génériques. Il est vivement recommandé d’en abuser.

Un peu de lecture

Pour en savoir plus, n’hésitez pas à consulter ces liens :

Conclusion

Les décorateurs sont une fonctionnalité très puissante et très utilisée du langage Python. Python, lui-même, en propose de nombreux. Nous n’avons qu’abordé functools.wraps, mais il en existe beaucoup d’autres que vous rencontrerez certainement un jour dans vos projets. Cet article n’est qu’une simple introduction tant les possibilités offertes sont larges. Ils sont un point fort du langage, avec sa syntaxe et son écosystème. Ne vous en privez pas. À vos décorateurs !