decorateur – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Update du tuto sur les décorateurs http://sametmax.com/update-du-tuto-sur-les-decorateurs/ http://sametmax.com/update-du-tuto-sur-les-decorateurs/#comments Wed, 26 Aug 2015 14:27:15 +0000 http://sametmax.com/?p=16817 Lui aussi est en deux parties. La première était facile : uniquement les prints à changer. Les features de prog fonctionnelle n’ont pas beaucoup évolué entre Python 2 et 3 il faut dire.

Pour la partie 2, j’ai changé quelques phrases, mais surtout un exemple qui avait pété, non pas à cause de Python 3, mais parce qu’il tapait sur une page dont le contenu avait changé.

Bref, si vous avez envie de savoir comment écrire vos propres décorateurs, vous savez quoi faire.

Et sur une note complètement différente, voici à quoi ressemble un vaccum bed, une pratique sexuelle très particulière où la personne est installée dans un sceau de plastique qu’on l’on met sous vide pour le faire coller à la peau :

Ensuite chacun fait ce qu’il veut : anneau vibrant, dildo, huile chaude…

]]>
http://sametmax.com/update-du-tuto-sur-les-decorateurs/feed/ 1 16817
Un peu de fun avec les décorateurs http://sametmax.com/un-peu-de-fun-avec-les-decorateurs/ 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.

]]>
12648
Décorateur de trace http://sametmax.com/decorateur-de-trace/ http://sametmax.com/decorateur-de-trace/#comments Mon, 15 Apr 2013 17:33:32 +0000 http://sametmax.com/?p=5670 décorateurs et sur l'écriture des logs en python j'ai voulu mettre en pratique dans mon projet. C'est là que les ennuis ont commencé !]]> (Ceci est un post invité de xonop sous licence creative common 3.0 unported.)

Suite aux superbes articles sur les décorateurs et sur l’écriture des logs en python j’ai voulu mettre en pratique dans mon projet.
C’est là que les ennuis ont commencé !

Objectif :

Créer un décorateur qui me permette de tracer le passage dans certaines méthodes.
Celui-ci doit :

  • Être débrayable facilement.
  • Récupérer automatiquement le nom de la méthode, sa classe et son module.

Première étape : le logger

Avant toute chose mettons en place l’environnement pour pouvoir tracer en utilisant le module logging.
Pour simplifier les exemples, nous associons un seul handler de type terminal.
La fonction log_debug permet de faire appel au logger pour tracer une information.

logger.py :

import functools
import logging

__DECO_ACTIVATED = True
__logger = None

def init_logger():
    global __logger
    __logger = logging.getLogger()
    __logger.setLevel(logging.DEBUG)
    terminalHandler = logging.StreamHandler()
    terminalHandler.setLevel(logging.DEBUG)
    __logger.addHandler(terminalHandler)

def log_debug(text):
    __logger.debug(text)

Deuxième étape : le décorateur, version basique

Pour commencer, le décorateur débrayable :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        __logger.debug("BEGIN")
        data = func(*args, **kwargs)
        __logger.debug("END")
        return data
    return wrapped

Remarques :

  • La fonction est décorée selon la valeur de la variable __DECO_ACTIVATED.
  • La fonction wraps du module functools reporte les attributs de la fonction originale sur la version wrappée (dont les docstrings).

Et maintenant un module pour tester ça :

main.py :

import logger

class Generic():
    @logger.log_decorator
    def do_it(self, arg1):
        logger.log_debug(arg1)

if __name__ == "__main__":
    logger.init_logger()
    generic = Generic()
    generic.do_it("NOW")

Et voilà le résultat :

BEGIN
NOW
END

Troisième étape : les noms de module et de fonction

Ces informations se récupèrent facilement, voici la nouvelle version du décorateur :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    func_name = func.__name__

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        msg = "Module={} Function={}".format(module_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
    return wrapped

Et maintenant le résultat :

BEGIN Module=__main__ Function=do_it
NOW
END Module=__main__ Function=do_it

Jusqu’ici tout va bien.

Quatrième étape : le nom de la classe

Et c’est maintenant que les choses se corsent !
Tout d’abord, l’objet func que nous manipulons est une fonction et non une méthode de classe :

print(func)

En effet lors de l’exécution du décorateur, la classe en tant qu’objet n’existe pas encore.
Il n’y a pas de lien direct entre la fonction et sa future classe.

Bon nombre de développeurs bien intentionnés nous conseillent d’utiliser self pour déterminer sa classe.

Oui mais :

  • Si la fonction n’est pas une méthode, pas de self !
  • La classe n’est pas obligatoirement celle de la méthode. Il peut s’agir d’une classe héritée.

Pour s’en convaincre, voici le nouveau décorateur :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    func_name = func.__name__

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        try:
            class_name = args[0].__class__.__name__
        except IndexError:
            class_name = ""
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
    return wrapped

Et maintenant le module de tests :

main.py :

import logger

class Generic():
    @logger.log_decorator
    def do_it(self, arg1):
        logger.log_debug(arg1)

class Specific(Generic):
    @logger.log_decorator
    def do_it(self, arg1):
        super().do_it(arg1)

if __name__ == "__main__":
    logger.init_logger()
    specific = Specific()
    specific.do_it("NOW")

Et le résultat tant attendu :

BEGIN Module=__main__ Class=Specific Function=do_it
BEGIN Module=__main__ Class=Specific Function=do_it
NOW
END Module=__main__ Class=Specific Function=do_it
END Module=__main__ Class=Specific Function=do_it

Et là c’est le drâme, on a perdu la classe Generic ! Mais analysons plutôt l’exécution :

  • Appel de la méthode Specific.do_it() vu que l’objet generic est une instance de cette classe.
  • Appel de la méthode Generic.do_it() par le biais de l’instruction super().do_it()

Comme prévu, la classe de l’objet self ne change pas que l’on soit dans une méthode de la classe ou de l’une de ses super-classes.

Quatrième étape : autre approche

Ne pouvant déterminer directement la classe d’appartenance de la fonction, essayons de la chercher dans le module.
Pour commencer il nous faut l’objet module alors que nous ne connaissons que son nom.
Le module inspect propose justement ce service grâce à getmodule.
Voici le décorateur modifié :

logger.py :

import inspect

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)
    class_name = "UNKNOWN"
    for key, obj in module_obj.__dict__.items():
        try:
            members = obj.__dict__
            method = members[func.__name__]
            if method == func:
                class_name = key
                break
        except (KeyError, AttributeError):
            pass
    func_name = func.__name__

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data

    return wrapped

Pour rechercher la fonction, le décorateur doit :

  • Lister les objets du module.
  • Rechercher pour chaque objet le nom de la fonction dans les attributs de celui-ci. Si l’attribut correspond à notre fonction, c’est gagné !

Nous obtenons alors :

BEGIN Module=__main__ Class=UNKNOWN Function=do_it
BEGIN Module=__main__ Class=UNKNOWN Function=do_it
NOW
END Module=__main__ Class=UNKNOWN Function=do_it
END Module=__main__ Class=UNKNOWN Function=do_it

Pas glop ça marche pas !
Regardons le contenu du module avant la recherche :

print(module_obj.__dict__.keys())
dict_keys(['__builtins__', '__name__', '__file__', '__doc__', '__loader__',
           '__cached__', 'logger', '__package__'])

Curieusement les classes n’apparaissent pas, mais c’est tout à fait normal.
Comme vu précédemment, lors de l’exécution du décorateur la classe est en instance de création.
Il faut donc faire cette recherche lors de l’exécution de la fonction wrappée.

Le décorateur devient donc :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)
    func_name = func.__name__

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        class_name = "UNKNOWN"
        for key, obj in module_obj.__dict__.items():
            try:
                members = obj.__dict__
                method = members[func.__name__]
                if method == func:
                    class_name = key
                    break
            except (KeyError, AttributeError):
                pass
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data

    return wrapped

Et le résultat :

BEGIN Module=__main__ Class=UNKNOWN Function=do_it
BEGIN Module=__main__ Class=UNKNOWN Function=do_it
NOW
END Module=__main__ Class=UNKNOWN Function=do_it
END Module=__main__ Class=UNKNOWN Function=do_it

Pas glop 2 le retour !
Faisons appel au débogueur suprême : print

key    = Generic
method = 
func   = 

key    = Specific
method = 
func   = 

Effectivement les références ne correspondent pas, et encore une fois, rien de plus normal.
La fonction d’origine a été wrappée donc celle présente dans le module n’est plus celle d’origine.
Qu’à cela ne tienne, recherchons-là !

Après la déclaration de la fonction wrapped, le décorateur la mémorise dans la variable wrapped_function.
Elle sera utilisée à l’exécution de la fonction.

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)
    func_name = func.__name__

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        class_name = "UNKNOWN"
        for key, obj in module_obj.__dict__.items():
            try:
                members = obj.__dict__
                method = members[func.__name__]
                if method == wrapped_function:
                    class_name = key
                    break
            except (KeyError, AttributeError):
                pass
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data

    wrapped_function = wrapped
    return wrapped

Et maintenant le résultat :

BEGIN Module=__main__ Class=Specific Function=do_it
BEGIN Module=__main__ Class=Generic Function=do_it
NOW
END Module=__main__ Class=Generic Function=do_it
END Module=__main__ Class=Specific Function=do_it

Ouf ! Ca marche !

Au menu du prochain épisode : logger la valeur de certains paramètres passés à la fonction décorée.

Xavier O. avec l’aide précieuse de Laurent B.

]]>
http://sametmax.com/decorateur-de-trace/feed/ 14 5670