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.
Les signaux de django font pas la mm chose ? A part que les receivers sont des décorateurs (sans doute plus de liberté … )
Et en général, pour faciliter le débogage notamment, on n’hésitera pas à utiliser http://docs.python.org/library/functools.html#functools.wraps pour conserver/rétablir le nom de la fonction « enveloppée ».
@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.
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à :
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 :
Ce qui revient à faire ça :
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.
^^
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 ?
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.