Le pattern observer en utilisant des décorateurs


Nous avons vu précédemment que les décorateurs permettaient d’exécuter du code avant et après une fonction, sans modifier la fonction. La plupart du temps on retourne ainsi une nouvelle fonction avec un comportement différent.

Mais il existe d’autres usages pour les décorateurs, et notamment un qui est au cœur du fonctionnement de django-quicky: l’abonnement.

def evenement(nom):
 
    # on assure que la liste des events et callabcks est initialisae
    evenement.abonnements = getattr(evenement, 'abonnements', {})
 
    # on ajoute un moyen d'appeler tous les callbacks pour un event
    evenement.trigger = lambda e: [f(e) for f in evenement.abonnements[e]] 
 
    # définition du décorateur lui-même
    def decorateur(func):
 
        # on ajoute la fonction comme callback pour cet event
        evenement.abonnements.setdefault(nom, []).append(func)
 
        # et on retourne la fonction telle qu'elle, sans la modifier
        return func
 
    return decorateur

Ce morceaux de code s’utilise ainsi:

# a chaque fois qu'on met le decorateur
# la fonction est liae à un événement
@evenement('evenement1')
@evenement('evenement2')
def reagir_a_evenement(evenement):
    # la fonction doit acccepter l'evenement en paramètre
    print "Oh, evenement '%s' a eu lieu" % evenement
 
@evenement('evenement1')
def moi_aussi(evenement):
    print "Cool, moi aussi j'ai reagit a l'evenement '%s'" % evenement
 
# ici on déclenche l'événement sans raison
# mais dans du vrai code on le déclenche à la suite
# d'une action réelle
evenement.trigger('evenement1')
evenement.trigger('evenement2')

Ici c’est un exemple simplifié, mais le principe y est: chaque fois qu’on utilise un décorateur, on associe la fonction décorée à un nom (ici le nom de notre événement, mais dans django-quicky, c’est l’url). Et quand l’événement se produit, on appelle toutes les fonctions enregistrées, en leur passant l’objet événement (ici une simple string) en paramètres.

Avec ce design pattern appelé ‘observer’, on découple complètement le code qui déclenche un événement (la lecture d’un fichier, une erreur, un entrée utilisateur, etc) et le code qui réagit à cet événement (qui peut très bien être celui d’une lib complètement séparée).

Le décorateur Python est un moyen particulièrement pratique de déclarer un abonnement d’une fonction à un événement, et ne demande quasiment aucun effort de la part du développeur qui l’utilise à part d’avoir une fonction qui accepte les bons paramètres pour réagir à tous les événements que peuvent produire votre library.

7 thoughts on “Le pattern observer en utilisant des décorateurs

  • DSeed

    Les signaux de django font pas la mm chose ? A part que les receivers sont des décorateurs (sans doute plus de liberté … )

  • Sam Post author

    @DSeed: oui, exactement.

    @Soli: Il faut apprendre à ne pas mettre ce genre d’informations dans un exemple pédagogique. On parle de wraps dans le tutos sur les décorateurs, ici la notion expliquée est le pattern observer. Top d’info rend les explications confuses.

  • Recher

    C’est très chouette.

    Dans le cas où vous voudriez mon opinion personnelle que j’ai, je vous dirais que je trouve cette ligne là :

    evenement.abonnements = getattr(evenement, 'abonnements', {})

    un peu bourrine et pas super compréhensible.

    Si j’ai bien tout compris, le but, c’est d’initialiser l’attribut “abonnements” à un dictionnaire vide, dans le cas où il n’existe pas encore. Si il existe, on le laisse comme il est.

    Si l’attribut existe déjà, ça revient à faire ça :

    evenement.abonnements = getattr(evenement, 'abonnements')

    Ce qui revient à faire ça :

    evenement.abonnements = evenement.abonnements

    On affecte l’objet à lui-même.
    Ca ne dérange absolument pas le python de faire quelque chose de ce genre. Mais ça dérange l’humain qui lit le code. Il risque de se dire : “WTF ? C’est quoi l’intérêt ?”. (Alors que le vrai intérêt de cette ligne se trouve uniquement dans le cas où l’attribut n’existe pas).

    Bref, moi je propose ça. C’est en deux lignes, mais ça me semble plus clair.

    if not hasattr(evenement, 'abonnements'):
        evenement.abonnements = {}
  • anthony

    Si je me trompe pas implémenté comme ça c’est bloquant, e.g. j’ai une seule fonction abonnée à ‘evenement1’, si je trigger deux fois de suite cet l’event

    evenement.trigger(‘evenement1’)
    evenement.trigger(‘evenement1’)

    Et que la fonction abonnée bloque:

    @evenement(‘evenement1’)
    def reagir_a_evenement(evenement):
    print “Oh, evenement ‘%s’ a eu lieu” % evenement
    time.sleep(60)

    Je verrai les 2 prints à 1min d’interval. Quelle serait la bonne solution pour faire qqch de complètement asynchrone ? Utiliser dans le décorateur evenement une thread pour lancer la fonction ?

  • Sam Post author

    Oui, mais avec le GIL, on n’a pas d’options non bloquantes valables en Python, c’est une des limitations du langage.

    Il n’existe pas de possibilité de faire quelque chose de complètement asynchrones, même en utilisant des threads. Au mieux on peut faire:

    – des threads / des coroutines qui au moins évitent de bloquer les IO.
    – utiliser le module multiprocessing, mais ça veut dire créer un process pour chaque trigger.

    Et dans tous les cas, ça veut dire des races conditions possibles sur les ressources partagées.

    Bref, si on veut vraiment un truc asynchrone en Python (c’est rare, mais ça arrive), on doit se tourner vers des frameworks types twisted ou tornado, qui eux implémente le truc en C derrière.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.