Sam & Max » callback http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Un peu de fun avec les décorateurs http://sametmax.com/un-peu-de-fun-avec-les-decorateurs/ http://sametmax.com/un-peu-de-fun-avec-les-decorateurs/#comments Mon, 17 Nov 2014 01:06:51 +0000 http://sametmax.com/?p=12648 Puisque la programmation asynchrone est au goût du jour, on se mange des callbacks un peu partout. Et ça alourdit toujours le code. Chaque langage, lib ou framework a essayé de trouver des astuces pour rendre tout ça plus digeste, et on a vu la naissance des Futures, Deferred, Promises, coroutines, yield from et autres joyeusetés.

Prenons par exemple un script Twisted. Déjà, Twisted, c’est pas vraiment l’exemple de la syntaxe Weight Watcher, ou alors si, mais avant le début du régime.

# -*- coding: utf-8 -*-
 
""" Télécharge des pages et affiche leur, de manière asynchrone """
 
import re
 
# Ceci doit être pip installé
import treq
from twisted.internet.task import react
from twisted.internet.defer import inlineCallbacks, returnValue
 
# Soit on utilise la syntaxe 'inlineCallbacks', c'est à dire avec des yields
# qui marquent les appels asynchrones.
@inlineCallbacks
def get_title(url):
    res = yield treq.get(url) # Ceci est asynchrone et non bloquant
    html = yield res.content() # Ça aussi
    try:
        val = re.search(r'', html.decode('utf8')).groups()[0]
    except:
        val = ''
 
    returnValue(val)
 
# Soit on récupère un objet defer et on ajoute un callback manuellement
def main(reactor):
 
    # Ceci est asynchrone et non bloquant
    defer = get_title('http://sametmax.com/quest-ce-quun-callback/')
 
    # Ceci arrive une fois que get_title est terminé
    def cb(title):
        print(title.upper() + '!')
 
    defer.addCallback(cb)
 
    # Pareil
    autre_defer = get_title('https://github.com/sametmax/django-quicky')
 
    def cb(title):
        print(title.upper() + '!!!')
 
    autre_defer.addCallback(cb)
 
    return defer
 
react(main)

D’une manière générale, je préfère la syntaxe à base de yields, même si elle oblige à se trimbaler le décorateur inlineCallbacks partout, à parsemer sa fonction de yields et à utiliser returnValue à la place de return puisque le mot clé est interdit dans les générateurs en Python 2.7.

Mais bon, ça reste facile à lire. On sait que les lignes avec yield, sont les appels bloquant qu’on demande à la boucle d’événements de traiter de manière asynchrone.

La syntaxe à base de callbacks est plus lourde, en revanche elle donne le contrôle sur la concurrence des callbacks puisqu’ils sont explicites au lieu d’être automatiquement ajoutés par magie. Elle parlera aussi plus aux dev Javascript qui ont l’habitude d’ajouter des callbacks manuellement.

Néanmoins, en JS, on a des fonctions anonymes plus flexibles, et on ferait donc plutôt une truc du genre :

get_title(url).then(function(title){
    # faire un truc avec le résultat
})

Et bien il se trouve qu’avec Python, bien qu’on ne le voit pas souvent, on peut avoir cette idée de la déclaration de son appel asynchrone juste au dessus de son callback, en utilisant des décorateurs.

En effet, les décorateurs ne sont que du sucre syntaxique :

@truc
def bidule():
    chose

N’est en fait qu’un raccourci pour écrire :

def bidule():
    chose
 
bidule = truc(bidule)

Du coup, on peut prendre n’importe quelle fonction, ou méthode, et l’utiliser comme décorateur :

@react
def main(reactor):
 
    then = get_title('http://sametmax.com/quest-ce-quun-callback/').addCallback
    @then
    def cb(title):
        print(title.upper() + '!')
 
    then = get_title('https://github.com/sametmax/django-quicky').addCallback
    @then
    def cb(title):
        print(title.upper() + '!!!')
 
    return cb

Et en jouant avec functools.partial, on peut faire aussi des trucs rigolos.

Non pas que cette syntaxe soit le truc indispensable à connaître et à utiliser. Mais les gens n’y pensent jamais. On utilise pas assez les décorateurs.

Par exemple, combien de fois vous avez vu :

def main():
    print('Doh')
 
if __name__ == '__main__':
    main()

Certaines libs, comme begin, font des décorateurs pour ça :

def main(func):
    if __name__ == '__main__':
        func()

Et du coup, dans son prog:

@main
def _():
    print('Doh')

Comme souvent, c’est le genre de feature qui peut être abusée, mais c’est parfois sympa de rapprocher une action juste au dessus de la fonction qui va être dans ce contexte.

J’espère ainsi vous avoir inspiré pour mettre un hack ou deux en production détournant complètement l’usage des décorateurs et ajoutant quelques gouttes de plus dans le vase de la sécurité de votre emploi, ou votre licenciement.

]]>
http://sametmax.com/un-peu-de-fun-avec-les-decorateurs/feed/ 0
Un décorateur pour accepter les callbacks en Python 3 http://sametmax.com/un-decorateur-pour-accepter-les-callbacks-en-python/ http://sametmax.com/un-decorateur-pour-accepter-les-callbacks-en-python/#comments Mon, 12 Aug 2013 10:03:44 +0000 http://sametmax.com/?p=7081 Si vous lisez assidûment ce blog, et je n’en doute pas car il est génial, vous savez ce qu’est un décorateur et un callback. On a même vu comment créer un système de gestion d’événements en utilisant ces deux concepts.

Un des événements auxquels on veut réagir le plus souvent, c’est l’appel d’une fonction, donc en gros être capable de fournir un callback quand une fonction est appelée. On peut bien entendu coder la logique du callback dans chaque fonction et méthode que l’on met en œuvre, mais avec un peu d’astuce, on peut trouver une solution générique qui va couvrir Pareto des besoins.

L’idée, c’est donc de coder un décorateur :

def accept_callbacks(func):
 
    # on va stocker tous les callbacks là dedans
    callbacks = []
 
    @wraps(func)
    def wrapper(*args, **kwargs):
 
        # on appelle la fonction originale
        result = func(*args, **kwargs)
 
        # on appelle ensuite chaque callback en lui passant le resultat 
        # de la fonction ainsi que les paramètres qui avaient été passés
        for callback in callbacks:
            callback(result, *args, **kwargs)
 
        # et on retourne le résultat
        return result
 
    # on attache la liste des callbacks au wrapper pour y avoir accès depuis
    # l'extérieur
    wrapper.callbacks = callbacks
 
    return wrapper

Du coup, pour accepter des callbacks sur une fonction, il suffit de décorer la fonction :

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

Ensuite on écrit son callback avec la signature qui va bien :

def mon_callback(result, a, b):
    print("Ma fonction a été appelée avec a=%s et b=%s !" % (a, b))
    print("Elle a retourné le résultat '%s'" % result)

Et pour ajouter un callback, c’est juste une insertion dans la liste :

add.callbacks.append(mon_callback)

Derrière, chaque appel de la fonction va appeler également tous les callbacks :

print(add(1, 1))
## Ma fonction a été appelée avec a=1 et b=1 !
## Elle a retourné le résultat '2'
## 2
 
print(add(42, 69))
## Ma fonction a été appelée avec a=42 et b=69 !
## Elle a retourné le résultat '111'
## 111
 
add.callbacks.remove(mon_callback)
 
print(add(0, 0))
# 0

Et le plus fun, c’est que ça marche aussi sans rien modifier avec les méthodes :

def autre_callback(result, self, truc_important):
    print("Ma fonction a été appelée avec truc_important=%s !" % truc_important)
    print("Elle a retourné '%s'" % result)
 
 
class CaMarcheAussiAvecUneClass(object):
 
    def __init__(self, repeat=1):
        self.repeat = repeat
 
    @accept_callbacks
    def puisque_une_classe_a_des_methodes(self, truc_important):
        return truc_important.upper() * self.repeat
 
 
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.append(autre_callback)
 
instance1 = CaMarcheAussiAvecUneClass()
instance2 = CaMarcheAussiAvecUneClass(2)
 
print(instance1.puisque_une_classe_a_des_methodes("Le fromage de chèvre"))
## Ma fonction a été appelée avec truc_important=Le fromage de chèvre !
## Elle a retourné 'LE FROMAGE DE CHÈVRE'
## LE FROMAGE DE CHÈVRE
 
print(instance2.puisque_une_classe_a_des_methodes("Les perforeuses"))
## Ma fonction a été appelée avec truc_important=Les perforeuses !
## Elle a retourné 'LES PERFOREUSESLES PERFOREUSES'
## LES PERFOREUSESLES PERFOREUSES

Par contre, si on veut qu’un callback ne s’active que pour une instance donnée, alors il faut ruser un peu :

# le retrait d'un callback, c'est un simple retrait de la liste
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.remove(autre_callback)
 
def callback_pour_une_instance(result, self, truc_important):
    # on check que l'instance est celle que l'on veut
    if self is instance1:
        print("Ma fonction a été appelée avec truc_important=%s !" % truc_important)
        print("Elle a retourné '%s'" % result)
 
CaMarcheAussiAvecUneClass.puisque_une_classe_a_des_methodes.callbacks.append(callback_pour_une_instance)
 
print(instance1.puisque_une_classe_a_des_methodes("Les points noirs des coccinelles"))
## Ma fonction a été appelée avec truc_important=Les points noirs des coccinelles !
## Elle a retourné 'LES POINTS NOIRS DES COCCINELLES'
## LES POINTS NOIRS DES COCCINELLES
 
print(instance2.puisque_une_classe_a_des_methodes("Les panneaux sens uniques"))
## LES PANNEAUX SENS UNIQUESLES PANNEAUX SENS UNIQUES

Niveau perf, ce n’est pas optimal, et bien sûr, l’appel des callbacks est synchrone et blocant. Ce n’est pas un souci dans 90 % des cas, pour les autres cas, vous devrez faire le truc à la main. En même temps, dès qu’on a des problèmes de perf, les codes génériques fonctionnent rarement.

Je vais peut être rajouter ça dans batbelt moi…


Télécharger le code de l’article

]]>
http://sametmax.com/un-decorateur-pour-accepter-les-callbacks-en-python/feed/ 3
Envoi d’un email par logging en cas de plantage d’un script python (ou: comment faire bouffer u”\xe9″ à SMTPHandler) 9 http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/ http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/#comments Tue, 12 Mar 2013 08:48:23 +0000 http://sametmax.com/?p=5313 (Ceci est un post invité d’un débutant pour les débutants… sous licence creative common 3.0 unported.)

Il y a peu, je me suis mis à utiliser logging pour debugger mes scripts de débutant.

Si comme moi vous avez l’habitude de mettre des print partout pour trouver l’origine d’un problème, et qu’ensuite vous avez passé de longues longues longues minutes/heures à traquer ces foutus print pour dépolluer la sortie console, jetez un oeil à écrire des logs en python.

Après quelques (minutes/heures/jours) de prise en main (vite, quoi…), on se demande comment on a fait pour s’en passer si longtemps. C’est simple, je n’utilise plus les touches “p”, “r”, “i”, “n” et “t” de mon clavier. Elles sont toutes propres.

En plus, logging m’a ouvert de nouvelles perspectives, parmi lesquelles la possibilité d’envoyer les logs par mail. Pas besoin de tout recevoir par mail, mais si ce foutu script de m%@rde pouvait m’envoyer un email quand il plante avec la source détaillé du problème, ce serait super.

Log post mortem par email

Admettons que vous vouliez vérifier que les pensées qui vous traversent l’esprit sont safe for work. Vous écrivez un script génial qui fait le boulot.

Comme vous commencez à savoir y faire en python, vous avez même écrit votre propre exception à vous tout seul…

class NotSafeForWorkError(Exception):
    """
    Exception soulevée si une pensée est NSFW
    """
    def __init__(self, msg):
        self.msg = u"Danger! %s est NSFW." % msg
 
    def __str__(self):
        return self.msg.encode("utf-8")
 
# liste des pensées proscrites
# (échantillon, elle est beaucoup plus longue que ça en réalité)
NSFW = ["cul", "seins", "sametmax"]
 
# boucle de censure qui soulève une exception si une pensée déconne
for pensee in ["pause", "pipi", "sametmax"]:
    if pensee in NSFW:
        raise NotSafeForWorkError(pensee)
    print u"%s est SFW" % pensee
 
#sortie:
## pause est SFW
## pipi est SFW
## Traceback (most recent call last):
##   File "censure_setm3.py", line 30, in
##     raise NotSafeForWorkError(pensee)
## __main__.NotSafeForWorkError: Danger! sametmax est NSFW.

Ça marche du tonnerre! C’est là que vous vous dites que ce serait bien si le script envoyait le traceback par email à votre psy pour le prévenir que vous avez déconné.

A la maison, vous avez lu l’article de Sam sur les logs post mortem. Ça a l’air facile à faire.

Vous créez d’abord un logger qui enverra les logs de niveau critique par mail:

import logging
from logging.handlers import SMTPHandler
 
nom_loggeur = "test_nsfw"
 
# On crée un logger et on met le niveau à critique:
#  il ne tiendra compte que des logs de ce niveau
logger = logging.getLogger(nom_loggeur)
logger.setLevel(logging.CRITICAL)
 
# On crée le handler en lui passant les paramètres
#  nécessaires à l'envoi d'un email
mail_handler = SMTPHandler(
    # Host et port
    ('SMTP.GMAIL.COM', 587),
    # From
    "MOI@GMAIL.COM",
    # To (liste)
    ["QUELQU.UN@QUELQUE.PART"],
    # Sujet du message
    "Erreur critique dans %s" % nom_loggeur,
    # pour l'authentification
    credentials = ("MONEMAIL@GMAIL.COM", "MONSUPERPASSWORD"),
    secure = ())
 
# On met le handler à "critique".
# Il enverra donc par mail les messages de ce niveau
mail_handler.setLevel(logging.CRITICAL)
 
# On définit un formatter: date, nom du logger, niveau, message
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
# On associe le formatter au handler:
#  c'est lui qui formatera les logs de ce handler
mail_handler.setFormatter(formatter)
 
# ... et on associe le handler au logger:
#  il utilisera ce handler, qui émettra les logs critiques
logger.addHandler(mail_handler)

Ensuite vous redéfinissez sys.excepthook de manière à ce que l’exception soit logguée. La fonction convertit le traceback en string, la loggue au niveau critique et laisse l’exception continuer son chemin.

import sys
 
# la fonction qui remplacera sys.excepthook
def en_cas_de_plantage(type_except, value, tb):
 
    # Traceback permettra de formater l'exception.
    import traceback
 
    # Mise en forme de l'exception. Retourne la trace
    #  sous forme de str avec numéros de lignes et tout
    trace = "".join(traceback.format_exception(type_except, value, tb))
 
    # On loggue l'exception au niveau "critique",
    #  elle sera donc envoyée par email
    logger.critical(u"Erreur inattendue:\n%s", trace)
 
    # ... et on laisse le script se planter...
    sys.__excepthook__(type_except, value, tb)
 
    # on remplace sys.excepthook, et le tour est joué
    sys.excepthook = en_cas_de_plantage

Et voilà. Vous lancez le script, il se casse la gueule dès qu’il rencontre “sametmax”, votre psy reçoit un email avec le traceback, c’est cool.

Sauf que…

SMTPHandler ne gère pas unicode

Si au moment de la création de mail_handler, vous passez comme sujet à SMTPHandler:

u"%s s'est planté" % nom_loggeur

ou que dans votre fonction “en_cas_de_plantage” vous mettez:

logger.critical(u"Bébé a encore glissé dans son caca:\n%s", trace)

… autrement dit si vous avez une chaîne unicode qui ne peut pas être encodée en ascii, ça va pas marcher:

Traceback (most recent call last):
  File "envoie_mail_on_crash.py", line 84, in emit
    smtp.sendmail(self.fromaddr, self.toaddrs, msg)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 734, in sendmail
    (code, resp) = self.data(msg)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 501, in data
    self.send(q)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/smtplib.py", line 321, in send
    self.sock.sendall(str)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ssl.py", line 229, in sendall
    v = self.send(data[count:])
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ssl.py", line 198, in send
    v = self._sslobj.write(data)
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 62: ordinal not in range(128)
Logged from file envoie_mail_on_crash.py, line 163

D’où l’on déduit qu’il y a une tentative foirée d’encodage d’une chaîne unicode en ascii par la méthode “write” de ce mystérieux _sslobj, et que c’est une méthode “emit” quelque part dans handlers.py qui lui a passé la chaîne.

Fuck! Investiguons. Et essayons de régler ça par le haut de la pile.

Toi, là au fond! Oui, toi!

C’est la methode emit de SMTPHandler qui est responsable. Elle fait quoi cette méthode? Elle formate le “record”, crée un message et l’envoie à l’adresse mail spécifiée. Voilà le code:

def emit(self, record):
    """
    Emit a record.
 
    Format the record and send it to the specified addressees.
    """
    try:
        import smtplib
        from email.utils import formatdate
        port = self.mailport
        if not port:
            port = smtplib.SMTP_PORT
        smtp = smtplib.SMTP(self.mailhost, port)
 
        # C'est ici l'objet "record" est formatté
        msg = self.format(record)
 
        # Le message est construit ici. Il sera ensuite envoyé par smtp.sendmail
        # C'est là que le bât blesse
        msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
                        self.fromaddr,
                        ",".join(self.toaddrs),
                        # Notons ce getSubject(record). La méthode renvoie
                        #  le sujet du message, passé à la création
                        #  de mail_handler
                        self.getSubject(record),
                        formatdate(),
                        # le record formaté
                        msg)
 
        if self.username:
            if self.secure is not None:
                smtp.ehlo()
                smtp.starttls(*self.secure)
                smtp.ehlo()
            smtp.login(self.username, self.password)
 
        # Le mail et envoyé
        smtp.sendmail(self.fromaddr, self.toaddrs, msg)
 
        smtp.quit()
    except (KeyboardInterrupt, SystemExit):
        raise
    except:
        self.handleError(record)

Qu’est-ce qu’il se passe?

Après quelques tests genre:

    msg = self.format(record)
    # C'est pratique parfois un petit print
    print type(msg)
 
##

… on voit que la méthode format (qui structure votre log selon la forme passée à logging.Formatter) retourne un objet de type unicode si le message que vous avez loggué (logger.critical(u"% foiré!", trace)) contient de l’unicode.

Idem pour getSubject(record) qui, en l’état, ne fait que retourner le sujet du email tel que vous l’avez passé à l’instanciation de SMTPHandler.

Donc, quand msg est formaté, si le log ou le message sont en unicode, msg sera en unicode.

WTF!?

J’ai été surpris quand j’ai fini par piger comment ça fonctionne. Même pour un débutant comme moi ça a l’air bête.

J’y connais rien en email, mais il ne m’a pas fallu longtemps pour comprendre que, dans sa forme basique, un email c’est du ascii, point. Et que si on veut envoyer autre chose que du ascii, qui soit décodable de l’autre côté, il faut qu’il y ait les headers appropriés, que le contenu soit encodé, que sais-je encore…

Peut-être un truc programmé par des gars qui ne manipulent jamais autre chose que de l’anglais? Ils auraient essayé d’envoyer un mail en russes ou en suédois, ça se serait planté au premier test. Oui, oui, Sam, la lingua franca, tout ça…

Ou alors c’est pour des questions de compatibilité avec des versions antérieures de python qui ne contiendraient pas les ressources nécessaires pour formater correctement un mail? Aucune idée…

Comment régler ça?

Il mangea u”\xe9″ et il en redemanda

Le module email de la lib standard permet de créer des emails. On trouve dans email.mime une classe MIMEText qui nous conviendra parfaitement. On va donc subclasser SMTPHandler et réécrire la méthode emit pour qu’elle construise un message “RFC-compliant”, comme ils disent dans la doc.

class SMTPHandler_unicode(SMTPHandler):
 
    def emit(self, record):
        try:
            import smtplib
            from email.utils import formatdate
 
            # On importe MIMEText
            from email.mime.text import MIMEText
 
            port = self.mailport
            if not port:
                port = smtplib.SMTP_PORT # 25
            smtp = smtplib.SMTP(self.mailhost, port)
 
            msg = self.format(record)
 
            # Au moment de la création de l'objet par MIMEText,
            #  si msg est de type unicode, il sera encodé
            #  selon _charset, sinon il sera laissé tel quel
            message = MIMEText(msg, _charset = "utf-8")
 
            # On ajoute les headers nécessaires. S'il sont de type unicode,
            #  ils seront encodés selon _charset
            message.add_header("Subject", self.getSubject(record))
            message.add_header("From", self.fromaddr)
            message.add_header("To", ",".join(self.toaddrs))
            message.add_header("Date", formatdate())
 
            if self.username:
                if self.secure is not None:
                    smtp.ehlo()
                    smtp.starttls(*self.secure)
                    smtp.ehlo()
                smtp.login(self.username, self.password)
 
            # Envoi du message proprement encodé
            smtp.sendmail(self.fromaddr, self.toaddrs, message.as_string())
 
            smtp.quit()
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

Ce qui est intéressant avec MIMEText c’est que si vous déclarez un encodage (_charset = "utf-8"), et que vous passez une chaîne unicode a l’instanciation, il l’encodera selon _charset.

Idem si vous ajoutez un header en appelant add_header, ce qui permet dans notre cas de traiter correctement le sujet du mail.

L’appel à la méthode to_string retournera le message proprement encodé (en base64) pour le transfert, et les headers appropriés seront inclus.

Voilà, il ne reste plus qu’à utiliser cette classe pour créer mail_handler, et le tour est joué.

mail_handler = SMTPHandler_unicode(
    # host et port
    ('SMTP.GMAIL.COM', 587),
    # From
    "MOI@GMAIL.COM",
    # To (liste)s
    ["QUELQU.UN@QUELQUE.PART"],
    # sujet du message
    u"Bonjour Mr Freud. %s a encore repéré des pensées génantes" % nom_loggeur,
    # pour l'authentification
    credentials = ("MONEMAIL@GMAIL.COM", "MONSUPERPASSWORD"),
    secure = () )

Bilan de l’histoire: c’est en débutant qu’on devient débutant…


Télécharger le code de l’article.

]]>
http://sametmax.com/envoi-dun-email-par-logging-en-cas-de-plantage-dun-script-python-ou-comment-faire-bouffer-uxe9-a-smtphandler/feed/ 9
Log post mortem avec Python 8 http://sametmax.com/log-post-mortem-avec-python/ http://sametmax.com/log-post-mortem-avec-python/#comments Thu, 07 Mar 2013 11:25:58 +0000 http://sametmax.com/?p=5260 Il y a des mois de ça, j’avais écris un article sur atexit, un module qui permet de lancer une fonction à la fermeture de la VM Python.

Ces fonctions sont appelées même si la VM s’arrête brutalement, mais on a aucune information sur la raison de l’arrêt de la machine virtuelle Python. Et elles sont exécutées même si tout s’est bien passé.

Si vous voulez réagir au plantage de votre programme, et seulement au plantage, tout en ayant en plus des informations sur la nature du foinage :

import sys
 
def on_crash(type, value, tb):
    print type
    print value
    print tb
 
sys.excepthook = on_crash
 
declencher_erreur = 1 + "1"
 
## <type 'exceptions.TypeError'>
## unsupported operand type(s) for +: 'int' and 'str'
## <traceback object at 0x00543AF8>

Ca peut être très intéressant pour débugger un processus détaché comme par exemple votre serveur WSGI qui fait tourner Django qui décide de se petit-suicider : demandez à sys.excepthook de faire un dump de l’exception dans un fichier log , et vous pourrez voir ce qui a propoqué la crise.

Si vous êtes du genre poli, vous voudrez quand même garder l’ancien comportement de sys.excepthook, qui est toujours disponible depuis sys.__excepthook__ :

def on_crash(type, value, tb):
    # faire ce que vous voulez ici puis...
    sys.__excepthook__(type, value, tb)

Et dire que vous commenciez à croire que vous saviez presque tout sur Python. Mouarf.

]]>
http://sametmax.com/log-post-mortem-avec-python/feed/ 8
Explication de code : callback à la mise à jour d’un array Numpy 2 http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/ http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/#comments Thu, 30 Aug 2012 14:32:13 +0000 http://sametmax.com/?p=1939 l'explication de code, voici un petit bout de numpyries envoyé par un lecteur.]]> Toujours dans l’esprit de l’explication de code, voici un petit bout de numpyries envoyé par un lecteur.

Lisez l’article sur les callbacks si vous n’êtes pas familiers avec le principe, et en avant:

# on importe numpy, une bibliothèque spécialisée dans le calculs impliquant de grands
# jeux de nombres et très utilisée par les scientifiques
import numpy as np
 
# On créé une classe qui hérite du type np.ndarray qui est un type d'array
# de taille fixe, et dont tous les objets doivent être du même type,
# mais qui peut avoir plusieurs dimensions et qui est très rapide à manipuler
# Notez que par convention NumPy n'utilise pas de majuscule pour le nom de 
# ces types pour matcher str, int, unicode, etc. Pour rester congruent,
# la classe enfant ne le fait pas non plus. Ne prenez pas cette habitude, 
# c'est un cas particulier.
class cbarray(np.ndarray):
 
 
    # __new__ est la seule méthode de classe par défaut, sans déclaration de @classmethode, 
    # donc le premier argument sera la classe en court. Normalement la convention
    # est d'appeler cet argument cls, mais ici l'auteur fait à sa sauce...
    #
    # Le reste des arguments sont les mêmes que pour __init__: ce sont ceux
    # attendus à l'instanciation de la classe cbarray:
    #   * data sont les données qu'on veut mettre dans l'array
    #   * cb est un callback qu'on appelera à chaque mise à jour de l'array
    #   * dtype est le type des données à mettre dans l'array (si on veut bypasser l'autodétection)
    #   * copy pour présicer si on veut copier les données, ou juste liér les données existantes
    # 
    # __new__ est appelée avant __init__: elle prend les paramètres qu'attend __init__, 
    # fabrique une instance, la retournepuis __init__ est appelée avec l'instance. 
    # On a rarement besoin d'overrider __new__, en générale __init__ est un meilleur choix
    # car c'est plus simple. Mais certains types NumPy ne permettent pas de faire 
    # autrement.
    def __new__(subtype, data, cb=None, dtype=None, copy=False):
 
        # On attache le callback à la CLASSE, et non à l'instance.
        # Ca peut paraitre étrange, mais on verra plus bas pour
        subtype.__defaultcb = cb
 
        # Selon que l'on souhaite copier les données, ou juste les lier
        # on créé une instance d'un type différent.
        # Notez que l'absence d'espace atour de "," et "=" n'est pas recommandé
        # pas le PEP8
        if copy:
            data = np.array(data,dtype=dtype)
        else:
            data = np.asarray(data,dtype=dtype)
 
        # Cette astuce permet d'utiliser un des deux types du dessus
        # mais au travers de l'API du type "subtype", c'est à dire notre 
        # classe.
        data = data.view(subtype)
 
        # On retourne l'instance ainsi créé.
        # Quand l'utilisateur fera cbarray(....), c'est cette instance
        # qu'il recevra
        return data
 
    # Juste une méthode qui appelle le callback si il existe
    def _notify(self):
        if self.cb is not None:
            self.cb()
 
    # Une propriété qui retourne un attribut de l'objet
    # en s'assurant qu'on utilise celui du parent.
    # C'est une supposition mais je pense que shape est une propriété du
    # parent, qu'elle n'est pas dispo sur le type array ou asarray, et 
    # que l'astuce data.view ne suffit pas à faire un proxy de celle-ci.
    # Donc je pense que ça sert à donner accès à cette donnée.
    def _get_shape(self):
        return super(cbarray, self).shape
    shape = property(_get_shape)
 
    # __setitem__ est une méthode "magique", appelée automatiquement qu'on
    # fait array[item] = val
    # Ici on l'utilise pour appeler _notify() à chaque mise à jour de l'array
    # et donc d'appeler le callback à chaque mise à jour.
    # Il est dommage de ne pas passer de paramètre à la méthode, comme 
    # l'ancienne valeur, l'item et la nouvelle valeur. Le callback va être
    # du coup assez limité. Mais ça suffira si par exemple tout ce qu'on
    # veut faire c'est écrire dans un fichier à chaque modification.
    def __setitem__(self, item, val):
        np.ndarray.__setitem__(self, item, val)
        self._notify()
 
    # NumPy permet de créer des sous types à partir du type de base, c'est
    # d'ailleurs une manière très courrante de créer des nouveaux conteneurs
    # de données. Mais ce faisant, NumPy bypass le mécanisme d'instanciation,
    # et __new__ et __init__ ne sont donc pas appelées. Pour y pallier, NumPy
    # ajoute la méthode __array_finalize__ qui est toujours appelée quand
    # un array est prêt à être utilisé. Elle peut être utilisée pour effectuer
    # un traitement pour chaque array créé, quelque soit sa provenance.
    # Ici, on l'utilise pour attacher le callback à "l'instance".
    # Souvenez-vous, plus haut on avait attaché le callback à la CLASSE.
    # Cette classe peut derrière être la source de nombreux arrays même si
    # __init__ n'est pas appelé pour eux :-(
    # La solution de l'auteur est donc de passer le callback à __new__, de 
    # l'attacher à la classe, et à travers __array_finalize__, de l'attacher
    # à "l'instance". Il faut garder en tête que tout nouvel appel à __new__
    # écrasera le callback pour toutes les instances suivantes. Mis à part
    # cela, ceci garanti que tout array aura le callback, et donc que 
    # _notify aura accès au callback, et donc que __setitem__ déclenchera le 
    # callback, et donc que la fonction sera bien appelée à chaque mise à jour
    # de l'array
    def __array_finalize__(self,obj):
        if not hasattr(self, "cb"):
            # The object does not yet have a `.cb` attribute
            self.cb = getattr(obj,'cb',self.__defaultcb)
 
    # Encore une méthode "magique" ajoutée par NumPy. Elle est appelée
    # quand on sérialise l'array et retourne des informations sur l'état
    # de l'array
    def __reduce__(self):
        object_state = list(np.ndarray.__reduce__(self))
        subclass_state = (self.cb,)
        object_state[2] = (object_state[2],subclass_state)
        return tuple(object_state)
 
    # inverse de __reduce__
    def __setstate__(self,state):
        nd_state, own_state = state
        np.ndarray.__setstate__(self,nd_state)
 
        cb, = own_state
        self.cb = cb
 
# Le callback qui doit être appelé à chaque mise à jour de l'array
# Je ne pense pas que ça puisse marcher, car il attend un argument,
# ce que _notify() ne lui passe pas.
def callback(arg):
    print 'array changed to',arg
 
 
# une petit démo du code si on run le script au lieu de l'importer
if __name__ == '__main__':
    x = cbarray([1,2,3], cb=callback)
    x[[0,1]] = 1.0
]]>
http://sametmax.com/explication-de-code-callback-a-la-mise-a-jour-dun-array-numpy/feed/ 2
Qu’est-ce qu’un callback ? 18 http://sametmax.com/quest-ce-quun-callback/ http://sametmax.com/quest-ce-quun-callback/#comments Wed, 22 Aug 2012 17:19:07 +0000 http://sametmax.com/?p=1831 mais c'est simple, il suffit de passer un callback".]]> mettreOn nous a parfois reproché de ne pas faire assez de tutos pour débutant. C’est pas faux, d’autant que quand j’ai commencé j’étais bien content que le site du zéro ait choisi de se spécialiser là dedans. Les tutos pour débutant sont vraiment la pierre angulaire de l’attractivité d’une techno.

Donc, un jour vous vous baladez avec vos premiers succès en prog, vous vous chauffer à utiliser une library externe (ce qui fait toujours peur au début) et il y a un truc que vous ne savez pas faire. Vous posez la question sur un forum, et on vous répond: “mais c’est simple, il suffit de passer un callback“.

Doh.

Rappel: on peut passer des fonctions en argument

Une fonction, ce n’est pas juste un bout de code auquel on donne un nom. C’est une unité de programmation à elle toute seule, en tout cas dans les langages modernes, et on dit dans ce cas qu’elles sont des “first class citizen” (citoyen de première catégorie, quoi, du vrai, du dur, du pur).

En pratique, ça veut dire qu’on peut manipuler la fonction sans l’éxécuter. En python ça donne ça:

>>> def dis_bonjour():
...     print "bonjour"
...
>>> print dis_bonjour # afficher l'objet fonction
<function dis_bonjour at 0x7f8cc6fce578>
>>> dis_bonjour.func_name # afficher le nom de la fonction
'dis_bonjour'

Ca veut dire aussi qu’on peut passer une fonction comme argument:

>>> def fonction_qui_appelle_une_fonction(fonction_a_appeler):
...     fonction_a_appeler()
...
>>> fonction_qui_appelle_une_fonction(dis_bonjour)
bonjour

Mais Za Koi ça sert ?

Et bien à dire qu’on va exécuter du code, même si on ne sait pas encore à l’avance quel est ce code. C’est très utile quand on code soit-même une bibliothèque pour permettre aux utilisateurs de celle-ci d’exécuter du code durant le fonctionnement de notre algo, sans avoir à mettre la main dedans.

C’est exactement ce que font les callback (ou appel en retour, traduit grossièrement).

Un callback, c’est une fonction passée en paramètre, qui va être appelée à une condition. La condition est la plus souvent “quand ceci arrive” et “ceci” est le plus souvent “quand le traitement est terminé”. Donc la grande majorité des callbacks sont des fonctions qu’on passe à d’autres fonctions pour qu’elles soient exécutées quand le traitement est terminé.

Des exemples, des exemples !

Si vous faites une interface graphique, vous voulez qu’un clic sur un bouton déclenche une action. Cette action est souvent passée comme un callback.

Exemple avec ce petit programme Tkinter (la lib d’UI installée par défaut avec Python):

>> from Tkinter import * # import de tout TK
>>> root = Tk() # création de la fenêtre
>>> def crie_ta_joie(): # notre callback
...     print "Yo !"
...
>>> b = Button(root, text="Go", command=crie_ta_joie) # création d'un bouton
>>> b.pack() # placement du bouton
>>> root.mainloop() # mise en route de l'UI

crie_ta_joie est passée à la classe Button via le paramètre command. Quand on cliquera sur le bouton ‘Go’, le callback crie_ta_joie sera donc appelé. ‘Yo !’ sera affiché dans le terminal.

C’est ce qu’on appelle la programmation événementielle: on écrit des fonctions qui sont appelées quand des événements arrivent, ici le clic sur un bouton.

Et en javascript…

Si vous utilisez jQuery, vous utilisez déjà des callbacks partout.

Ainsi, si vous faites:

$.get('/arretez/ou/ma/mere/va/tirer', function(){
    alert('Bang !');
});

jQuery va faire une requête ajax sur l’url, et le programme va continuer de fonctionner car les appels réseaux sont non bloquant. Mais quand la réponse arrivera, le callabck sera appelé, et fera alert(Bang !).

Les callbacks sont donc très utilisés pour la programmation asynchrone, c’est à dire quand on ne connaît pas le temps que vont mettre des opérations à s’effectuer mais qu’on veut réagir une fois qu’elle sont terminées.

L’injection de dépendances

Les callbacks sont aussi très utilisés pour une technique de programmation appelée “injection de dépendances” qui consiste à permettre à ceux qui utilisent votre code de choisir ce que feront certains bouts de code.

Imaginez une fonction (ici assez simplifiée) de téléchargement qui permet d’afficher la progression de celui-ci:

import urllib2, sys
def download(truc_a_telecharger, fichier_de_destination):
 
    # on ouvre la connection
    u = urllib2.urlopen(truc_a_telecharger)
 
    taille_des_bouts = 8192
    total_telecharge = 0
 
    # on ouvre le fichier pour écrire ce qu'on télécharge
    with open(fichier_de_destination, 'w') as f:
 
        # while True / if : break est un palliatif Python
        # pour l'absence d'instruction "until"
        while True:
 
            # on télécharge un petit bout du fichier
            bout = u.read(taille_des_bouts)
            total_telecharge += taille_des_bouts
 
            if not bout: # plus rien à télécharge: on sort
                break
 
            # on écrit le bout de fichier téléchargé
            f.write(bout)
 
            # on écrit un point sur le terminal pour noter qu'un bout a été
            # téléchargé
            sys.stdout.write('.')

Qui s’utilise comme ça:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

On pourrait faire beaucoup mieux que juste afficher un point pour chaque bout de fichier téléchargé. On pourrait par exemple afficher un pourcentage d’avancement. Ou écrire dans un log. Ou ne rien faire, et supprimer tout affichage.

Mais on veut garder le comportement par défaut car on pense que la plupart des gens l’utiliseront ainsi, et qu’il n’y a pas de raison qu’ils le recodent.

En modifiant la fonction, et en permettant de passer un callback, on permet cette flexibilité:

def download(truc_a_telecharger, fichier_de_destination,
             # on attend un callback en paramètre
             # mais on en passe un par défaut
             afficher_le_progres=lambda *x, **y: sys.stdout.write('.')):
 
    u = urllib2.urlopen(truc_a_telecharger)
    # on chope la taille du fichier, ça permettra plus de choses
    taille_du_fichier = int(u.info().getheaders("Content-Length")[0])
    taille_de_bloc = 8192
    total_telecharge = 0
 
    with open(fichier_de_destination, 'w') as f:
 
        while True:
 
            # ici on appelle le callback en lui passant un maximum de paramètres
            # pour qu'il puisse faire le plus de chose possible
            afficher_le_progres(truc_a_telecharger, fichier_de_destination,
                                taille_du_fichier, total_telecharge)
 
            bout = u.read(taille_de_bloc)
            total_telecharge += taille_de_bloc
 
            if not bout:
                break
 
            f.write(bout)

Et on l’utilise comme avant:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

Ou avec plus de puissance:

def log(truc_a_telecharger, fichier_de_destination,
       taille_du_fichier, total_telecharge):
    with open('progress.log', 'w') as f:
        pourcentage = str(total_telecharge * 100 / taille_du_fichier)
        f.write(pourcentage)
 
download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', log)

Et si on veut supprimer tout affichage, on peut passe une fonction qui ne fait rien:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', lambda *x: None)

Il y a plusieurs choses importantes ici:

  • on accepte un callback en paramètre
  • le paramètre possède une valeur par défaut. Hé oui, on peut mettre une fonction en valeur par défaut !
  • la fonction est une fonction anonyme. Ce n’est pas obligatoire, mais c’est pratique.
  • la fonction par défaut utilise l’opérateur splat pour accepter un nombre illimité de paramètres, même si elle ne va pas les utiliser.
  • on délègue le comportement de l’algo lors de l’affichage du progrès à la fonction passée en paramètre.
  • on passe un max de paramètres à cette fonction pour lui donner le plus de libertés possible. C’est aussi pour ça que notre fonction accepte par défaut un nombre illimité de paramètres: sinon ça ferait une erreur

Ce système est l’injection de dépendance: on délègue une partie du travail à du code injecté depuis l’extérieur. Cela permet une extensibilité de notre fonction, sans sacrifier sa simplicité puisqu’on a une valeur par défaut.

On peut pousser l’injection très loin: en passant carrément des listes de fonctions, et toutes les appeler, ou des objets, et appeler plusieurs méthodes de l’objet (ce dernier point est une extension de l’injection de dépendance appelé le pattern strategy).

]]>
http://sametmax.com/quest-ce-quun-callback/feed/ 18