PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Réagir aux changements avec le module signals de Django

jeudi 1 novembre 2012 à 04:36

Le module signals contient une implémentation du design pattern observer, c’est à dire un moyen de lier un callback à un événement afin de pouvoir y réagir quand il se déclenche.

Les événements peuvent être aussi divers que “une requête a commencé” ou “un modèle a été sauvé en base de données”.

L’utilisation d’un signal se fait en 3 temps:

  1. décider à quel événement on veut réagir;
  2. décider ce qu’on veut faire quand il se déclenche;
  3. associer l’événement à l’action.

Par exemple, après la suppression d’un modèle, je veux faire un travail de nettoyage dans ma base de données.

Je choisis l’événement auquel je veux réagir dans la liste des signaux disponibles:

from django.db.models.signals import post_delete

Ici j’ai choisi post_delete, car c’est un signal qui est envoyé après la suppression d’un modèle automatiquement par Django.

Ensuite je décide de ce que je veux faire quand l’action se déclenche en écrivant une fonction:

def reaction_au_signal(sender, **kwargs):
    # faire ici mon opération de nettoyage

Vous noterez les arguments très spécifiques que j’accepte. Ils sont propres à l’implémentation des signaux de Django et cette signature est obligatoire. sender est l’objet qui envoie le signal. Dans notre cas ce sera la classe du modèle supprimé. kwargs c’est tout le reste, et son contenu dépend du type de signal reçu. Généralement il y a plein d’informations dedans sur le contexte qui nous permettent de prendre des décisions.

Ainsi, pour le signal post_delete, kwargs contient l’instance du modèle supprimé (dont les valeurs ne sont donc plus en base de données, ne faites pas de query avec !) et le nom de la base de données utilisés si vous en utilisez plusieurs.

Enfin il faut associer l’événement à l’action, en utilisant la fonction connect().

post_delete.connect(reaction_au_signal)

Le code complet donne ceci:

from django.db.models.signals import post_delete
 
def reaction_au_signal(sender, **kwargs):
    # faire ici mon opération de nettoyage dans la base de donnée
 
post_delete.connect(reaction_au_signal)

Réponses à quelques questions existentielles

Mais pourquoi que à quoi ça sert de quoi donc ?

Pourquoi ne pas utiliser plutôt l’héritage, ou mettre des hooks de callback ? Et bien tout simplement parce que les signaux permettent de réagir à une événement sans avoir à toucher le code qui génère l’événement. C’est un moyen très flexible et puissant de réagir à ce que fait du code dans une autre application. Ou de permettre à du code d’autres applications de réagir à vos événements sans avoir à toucher à votre code. Car oui, vous pouvez définir vos propres signaux.

Je les mets où ces signaux ?

Généralement dans le module qui contient la même sémantique que le signal: si c’est pour les requêtes HTTP, dans views.py, si c’est pour l’ORM, dans models.py, etc.

Est-ce que Dieu existe ?

Cher lecteur, merci de ne pas prononcer mon nom en vain.

Quelques astuces avec les signaux

Il est rare que vous vouliez réagir à la suppression de tous les modèles, ou de toutes les requêtes. Généralement on veut uniquement réagir à un signal pour un émetteur précis. Django permet cela en vous laissant choisir le sender:

 
class TopModel(models.Model):
    bonnet = models.TextField(max_char=1, default="A")
 
post_delete.connect(reaction_au_signal, sender=TopModel)

Ainsi, reaction_au_signal() ne sera appelée qu’à la suppression d’un objet de type TopModel.

Sachez aussi qu’il existe une syntaxe à base de décorateurs. Ca fait la même chose, mais perso je préfère la style:

from django.db.models.signals import post_delete
from django.dispatch import receiver
 
@receiver(pre_save, sender=MyModel)
def reaction_au_signal(sender, **kwargs):
    ...

Il faut aussi savoir que parfois, certains modules sont exécutés plusieurs fois, et si le handler du signal est déclaré dedans, il va être attaché plusieurs fois au signal, et donc lancé plusieurs fois quand le signal se déclenche. Pas glop.

Pour éviter ça, on peut passer un identifiant unique en attachant le signal:

post_delete.connect(reaction_au_signal, dispatch_uid="une_chaine_de_caracteres_quelconque")

dispatch_uid peut contenir n’importe quoi, pourvu qu’il soit unique à cette association signal/callback.

Il y a aussi un autre paramètre est nommé weak. C’est un booléen par défaut mis sur True qui décide si la référence vers le handler est une référence faible ou non. Mettez le sur False si votre callback n’a aucune autre référence que le signal, par exemple si la fonction est générée à la volée, ou si c’est une lambda.

Enfin, les signaux ne sont PAS exécutés dans un thread à part, donc ils bloquent l’exécution du programme comme le reste du code. La bonne nouvelle, c’est que ça vous permet d’utiliser pdb dedans. C’est particulièrement utile pour les signaux complexes comme m2m_changed qui est assez compliqué et pour lequel on s’y reprend à plusieurs fois.

Ah, juste un dernier détails. Model.objects.update() ne déclenche aucun signal…

m2m_changed, ce petit bâtard

La plupart des signaux sont super faciles à manipuler. Sauf un. m2m_changed. C’est un enculé.

Il faut savoir que quand vous utilisez models.ManyToManyField, une troisième table est automatiquement et silencieusement créé qui contient l’association entre vos deux modèles, avec le modèle correspondant. Mais ce modèle ne déclenche pas les signaux *_save et *_delete.

Donc si vous voulez réagir à Model.objects.add(), clear() et consorts, il va falloir se mapper sur m2m_changed, un espèce de fourre tout qui gère l’intégralité des cas de figures.

Il s’utilise comme les autres:

def handler(sender, **kwargs):
   # truc
 
m2m_changed.connect(handler)

Mais déjà, première différence, si vous voulez filtrer sur le sender, il faut utiliser le modèle autogénéré de la troisième table qui est un attribut de l’attribut du modèle qui définit la relation many to many. Vous suivez ? Non ? Relisez la phrase. Ça donne ça:

 
class Tic(object):
 
     partenaire = models.ManyToManyField(Tac)
 
def handler(sender, **kwargs):
   # truc
 
m2m_changed.connect(handler, sender=Tic.partenaire.through)

through contenant toujours le modèle voulu.

Ensuite, et c’est là la partie bien relou, kwargs va contenir en plus de instance et using:

action

Ce qui arrive pendant le signal. En gros au lieu d’avoir 6 signaux, on en a un auquel on passe la valeur: “pre_add, post_add, pre_remove, post_remove, pre_clear, post_clear“. Du coup votre code doit gérer tous les cas dans une seule fonction à grand coup de if/else. Génial !

reverse

Indique dans quel sens est la relation. Hyper confusionant.

En gros, si j’ai:

class Tic(object):
     partenaire = models.ManyToManyField(Tac)

reverse est sur False, si on part de Tic pour aller vers Tac, et True si on part de Tac pour aller vers Tic, car le ManyToManyField est déclaré dans Tic. C’est complètement arbitraire, car il n’y a bien entendu aucun sens à une relation M2M, c’est justement ce qui la différencie d’un M2One.

Et là où ça devient vraiment fendard, c’est que la valeur des autres paramètres changent selon la valeur de reverse. Il va falloir rajouter des if/else dans vos if/else.

model

La classe qui a été ajoutée, retirée ou wipée de la relation. Dans notre cas, si reverse, c’est Tic, sinon, c’est Tac.

pk_set

Donc le cas des actions add et remove (mais pas clear), ceci est la liste des objets concernés. Elles sont des id d’instances de Tic, si reverse, sinon des id d’instances de Tac.

Créer son propre signal

C’est tellement facile qu’au début on est pas sûr d’avoir tout fait.

Supposons que vous voulez faire une application qui envoie un POUET ! et que vous vouliez également permettre à une autre personne de réagir à cet événement indispensable.

Dans un fichier signals.py, vous allez écrire:

import django.dispatch
 
pre_pouet = django.dispatch.Signal(providing_args=["pouet"])
post_pouet = django.dispatch.Signal(providing_args=["pouet"])

Ouai, c’est tout. Vous avez créé deux signaux importables qui attendent un argument: le pouet.

Et pour déclencher ces signaux, c’est très simple:

from signals import pre_pouet, post_pouet
 
class PouetGenerator(object):
    ...
 
    def send_pouet(self, pouet="Alors là, je dis pouet !"):
 
        pre_pouet.send(sender=self, pouet=pouet)
        print pouet
        post_pouet.send(sender=self, pouet=pouet)

C’était vachement dur !

Et si quelqu’un veut réagir à vos pouets, il peut faire:

def pouet_handler(sender, **kwargs):
    print "Tiens, un pouet !"
 
post_pouet.connect(pouet_handler)

Dans la série des subtilités, vous avez, en plus de send(), la possibilités d’appeler send_robust(). C’est la même chose, mais les exceptions sont attrapées ce qui permet à tous handlers de recevoir le signal, même en cas d’erreur. Le sender reçoit les exceptions dans un tuple à la fin.

A ce stade là, vous aurez compris, handler, receiver et callback désignent la même chose: la fonction qui réagit à l’événement. Je les ai utilisées un peu partout sans y prendre garde dans l’article, donc je mets cette note pour le cas où je vous ai perdu.

Last word

Signal.disconnect() est l’inverse de connect(). Je ne m’en suis jamais servis.

Python, Ruby et PHP sont lents

mardi 30 octobre 2012 à 17:44

Ce matin je lançais ma petit recherche Python/Django/Git habituelle sur Twitter pour voir ce qui s’y tramait, quand je suis tombé sur plusieurs tweets qui m’ont fait tiqué.

Il y en a un qui résume très bien l’idée:

Cay de la merde je préfère les langages plus bas niveaux, python c’est lent.

C’est un argument que je lis souvent, et qui est aussi utilisé contre Ruby ou PHP.

Si j’ai la personne en face de moi, généralement la question qui suit est:

Ouai je comprends. C’est quoi ta contrainte ? Tu dois exécuter quel calcul sous quelle limite de temps ?

Il n’y a jamais aucune réponse autre qu’un bafouillement, car une personne qui a une réelle contrainte de temps d’exécution ne sort pas ce genre d’ânerie: il utilise le bon outil pour le bon travail selon des metrics précises, pas selon le nom du langage.

Ceux qui trollent, généralement des étudiants en informatique qui n’ont encore jamais codé d’utile de leur vie (je l’ai fais, donc je +1, d’ailleurs parfois je le fais toujours :-p), ont entendu / lu que Python, Ruby et PHP étaient lents, et donc les rejette car ils ne correspondent pas à l’idée qu’il se fait de la programmation, encore naissance dans sa tête.

La vérité est que les personnes qui ont des contraintes de temps d’éxécution auquel langage X, n’importe quel langage, ne peut pas répondre, font partie des 0.00000001 de la population des programmeurs: le plus souvent dans l’embarqué (voiture, satellite, microchips, etc) et les systèmes temps réels (navigation, chaîne de productions, bourse d’échange…). Pour les autres, nous, quasiment tous, les problèmes de performances se joueront sur l’algo, les libs, les serveurs Web et BDD, le caching, etc. On peut programmer en Basic ou en GOTO++, ça ne change rien.

Languages, libraries and frameworks don’t scale. Architectures do.

Cal Henderson

Dans certains cas, par exemple le calcul scientifique, les interfaces graphiques ou les jeux videos, la rapidité est importante, et on le sait sans même mesurer. Mais ces problèmes là sont résolus depuis longtemps: il existe des bindings en C extrêmement rapides qui permettent de coder dans son langage interprété favoris tout en bénéficiant d’algos Speedy Gonzalez sous le capot.

Par ailleurs, ça a déjà été dit 1000 fois, mais ça ne fait pas de mal de le répéter:

Le salaire du développeur et l’impact commercial de la lenteur d’un développement coûtent immensément plus cher qu’un ordinateur plus puissant. Qui d’ailleurs ne coûtera plus rien dans 6 mois.

Alors OK, ce n’est pas une invitation à coder avec ses pieds sous prétexte qu’on la puissance à disposition (ce qui se fait malheureusement de plus en plus).

Mais choisissez une techno parce que vous êtes productif avec, pas pour ses performances. Sauf si vous avez des mesures chiffrées qui s’imposent à vous. Auquel cas vous n’avez de toute façon pas besoin de lire cet article, vous êtes plus compétent que moi.

Pedobear et SNCF

mardi 30 octobre 2012 à 02:25

Ce matin dans le train, j’essayais de trouver l’inspiration pour un article de cul. Rien ne vint.

C’est que c’est dur pondre une news par jour, vous vous rendez pas compte. Faire un excellent article une fois, c’est facile. On a tous une étincelle de génie un jour ou l’autre. Mais être bon tous les jours, je cherche encore comment faire.

C’est entre autre pour ça qu’il est vachement plus impressionnant d’être un bon acteur de théâtre que de ciné.

Mais bon, cette fois le Dieu du porno infantile est venu à mon secours, sous la forme ici incarnée de deux petites voisines de 8 ans, qui me permettront de combler le vide d’aujourd’hui en vous reportant leur conversation:

  • tu as vu sa culotte ?, dit la première, tendant le chef-d’œuvre d’art néo-naïf en pastel qu’elle venait juste de terminer. Il représentait une humanoïde femelle à la jupe triangulaire fort drue, rouge et courte. Et avec pas mal de fils qui dépassaient.
  • nan, j’vois pas !
  •  j’l'ai pas dessiné !

Merci les enfants !

Retour d’expérience sur l’installation d’Ubuntu 12.10

dimanche 28 octobre 2012 à 14:09

Les versions d’Ubuntu sont de plus en plus instables avec le temps.

Premier laptop (update): pas de touchpad (obligé de le faire passer pour une souris USB avec une option dans un fichier de config GRUB) et pas de wifi (obligé de trouver et compiler le drivers à la main). Je vous passe les détails sur mon après-midi pour avoir trouvé les solutions à ces problèmes.

Deuxième laptop (fresh install): plus de gestion du second moniteur. 4 crash de 4 applications différentes en une demie-heure: 2 sous Unity, et 2 sous Gnome-shell.

Le daemon Ubuntu one est toujours aussi gourmand dès qu’il a plus de 3Go à indexer, et inkillable.

Je joue actuellement avec l’idée de retourner sur une debian et un bureau GNOME 2. J’ai quand même installé KDE pour faire mon ouvert d’esprit. J’ai refermé mon esprit après l’impression de viol graphique que j’ai ressenti à la connection.

Si un dev Ubuntu passe par là: les gars, arrêtez de rajouter des features de Web social et des fenêtres qui font wizz et prenez les 2 prochaines versions pour faire un produit stable. Sans nouvelles fonctionnalités. Justes les dernières libs (pas comme debian), mais un ensemble stable.

Pas la peine de de suggérer les autres distribs/bureaux, ceci n’est pas une appel à une solution à mon problème.

P.S: arrête de rire Max. Je te pisse à la raie.

P.P.S: vous noterez que le passage à Python 3 ne s’est pas fait. Et oui, c’est pas si simple

Comment recruter un développeur Python

samedi 27 octobre 2012 à 16:28

Bonjour M. Gentil. Vous venez pour la position de stagiaire ingénieur senior en periode d’essai sur 3 ans ?

C’est bien, c’est bien.

Nous avons des perspectives de progression fascinantes dans notre SS3I au carré.

J’ai juste quelques tests à vous faire passer. Trois fois rien. Simple formalité administrative. Vous comprenez, on ne peut pas embaucher pas n’importe quel Bac + 5 et le payer SMIC, comme ça sur un coup de tête.

Ce n’est pas contre vous, non.

Pourriez-vous me dire ce qu’affiche ce snippet ? (mouahahahahaha, rire diabolique intérieur)

def test():
 
    try:
        return 1 + "1"
    except TypeError:
        return "exception"
    finally:
        return "finally"
 
print test()

Et celui-là, il affiche la stack trace ou pas ?

def test():
 
    try:
        print 1 + "1"
    except TypeError:
        raise ValueError('Test')
    finally:
        return "finally"
 
print test()

Bon ok, mais si on a un générateur alors ?

def test():
 
    try:
        yield 1 + "1"
    except TypeError:
        yield 'typerror'
        return
    finally:
        yield "finally"
        return
 
    yield "Out"
 
for value in test():
    print value

Quelle exception sera catchée, dans le bout de code suivant ? (tapoter son style de manière énervante sur la table)

def test():
 
    try:
        assert 1 + "1"
    except AssertionError:
        print "assertionerror"
    except Exception:
        print "exception"
    except TypeError:
        print "typerror"
 
test()

Je vois. Donc finally est exécuté dans tous les cas ?

def test():
 
    def foo():
        foo()
 
    try:
        foo()
    except RuntimeError:
        print 'runtimeerror'
    finally:
        print 'finally'
 
test()

Vous êtes sûr ? VRAIMENT dans tous les cas ? (regard appuyé bien stressant, travaillé durant un poste de manager chez Quick)

def test():
 
    try:
        PRINTEUH !
    except SyntaxError:
        print 'syntaxerror'
    finally:
        print 'finally'
 
test()

VRAIMENT, VRAIMENT, dans tous les cas ? Mais alors, VRAIMENT ? (prendre la voix d’Alain Chabat, parce qu’arrivé à ce stade là c’est juste plus rigolo)

def test():
 
    def foo(bar):
        print bar
        return foo, bar
 
    bar = 'bar'
 
    try:
        while True:
            foo, bar = foo(bar)
 
    except RuntimeError:
        print 'runtimeerror'
    finally:
        print 'finally'
 
test()

Ne vous inquiétez pas, nous ne vous JUgeons paaaaaaas. Détendez-vous. Allez. Une petite dernière. Je vous aide. sys.exit(1) retourne le code 1.

import sys
 
def test(l=[]):
 
    print l
    try:
        return 1 + "1"
    except TypeError:
        return l or l.append(sys.exit(1))
    finally:
        print "Je sais plus là, sérieux"
        if len(l) < 1:
            test()
 
test()
test()

Bon, je sens que vous êtes fatigué. On va arrêter là peut être. Vous êtes le genre à vous en tenir là, hein ? Non. Très bien, très bien. Voici un code sur lequel on planche en interne depuis une semaine pour comprendre combien de fois il affiche finally, mais on s’est dit que vous pourriez le résoudre gratuitement pour nous:

import sys
import time
 
import multiprocessing
from Queue import Empty
 
in_queue = multiprocessing.Queue()
out_queue = multiprocessing.Queue()
 
def worker():
 
    while True:
 
        try:
            res = in_queue.get(timeout=0.1)
            if res == 'stop':
                sys.exit(1)
            print res
        except (Empty, multiprocessing.TimeoutError):
            pass
        finally:
            print 'finally'
 
 
process = multiprocessing.Process(target=worker)
process.start()
 
 
in_queue.put('test')
in_queue.put('stop')
 
print 'afterstop'
 
time.sleep(1)
 
print 'done'

Merci, ce sera tout.

Bon WE, monsieur Gentil.

Nous gardons votre CV. On vous recontactera.