decorator – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Un décorateur pour accepter les callbacks en Python 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 7081
Créer un décorateur à la volée http://sametmax.com/creer-un-decorateur-a-la-volee/ http://sametmax.com/creer-un-decorateur-a-la-volee/#comments Fri, 12 Jul 2013 05:39:00 +0000 http://sametmax.com/?p=6624 tout ce qu'il faut pour être au point. Néanmoins en informatique la moitié de la connaissance, c'est l'usage, pas la fonctionnalité. Car il y beaucoup d'usages auxquels on ne pense pas.]]> Sur les décorateurs, normalement, vous avez tout ce qu’il faut pour être au point.

Néanmoins en informatique la moitié de la connaissance, c’est l’usage, pas la fonctionnalité. Car il y beaucoup d’usages auxquels on ne pense pas.

Particulièrement, je vous avais déjà expliqué que les fonctions étaient des objets comme les autres en Python, et qu’on pouvait donc les créer à la volée, les retourner, les passer en paramètre, et même leur coller des attributs.

Or les décorateurs ne sont jamais que des fonctions.

Maintenant, souvenez vous, le décorateurs property permet de faire ceci :

class Mamouth(object):

    _valeur = "3 calots"

    @property
    def valeur(self):
        return self._valeur.upper()

    @valeur.setter
    def valeur(self, valeur):
        self._valeur = valeur.strip()

>>> bille = Mamouth()
>>> bille.valeur
u'3 CALOTS'
>>> bille.valeur = "une pépite           "
>>> bille.valeur
>>> print(bille.valeur)
UNE PÉPITE

La syntaxe qui doit attirer votre attention est @valeur.setter. En effet, d’où vient ce décorateur ?

On comprend mieux ce qui s’est passé avec ce test :

>>> Mamouth.valeur.setter

setter est tout simplement un attribut de la méthode valeur. Par ailleurs, c’est une fonction et un décorateur.

Pourquoi faire cela ? Et bien tout simplement parce que cela permet d’attacher une fonction qui ne sert que dans un cas (ici ça ne sert qu’à créer le setter de la propriété valeur) à son contexte.

Bien entendu vous pouvez faire ça vous même, il suffit de le vouloir très fort et de croire en le pouvoir de l’amour.

Par exemple, imaginez un décorateur qui permet d’attacher un comportement de sérialisation à une fonction. On ne veut pas modifier la fonction, mais on veut qu’elle puisse automatiquement, pour quelques caractères de plus, pouvoir aussi retourner du JSON ou du pickle.

import json
import pickle

def serializable(func):

    # Contrairement à la plupart des décorateurs, on ne va pas retourner
    # un wrapper, mais bien la fonction originale. Simplement on lui aura ajouté
    # des attributs

    func.as_json = lambda *a, **k: json.dumps(func(*a, **k))
    func.as_pickle = lambda *a, **k: pickle.dumps(func(*a, **k))

    return func

Et ça s’utilise ainsi :

import locale

from calendar import TimeEncoding, day_name, day_abbr

# obtenir les noms de jours localisés est complètement rocambolesque en python
def get_day_name(day_number, locale, short=False):
    """
        Retourne le nom d'un jour dans la locale sélectionnée.

        Exemple :

        >>> get_day_name(0,  ('fr_FR', 'UTF-8'))
        'lundi'
    """
    with TimeEncoding(locale) as encoding:
        s = day_abbr[day_number] if short else day_name[day_number]
        return s.decode(encoding) if encoding is not None else s

@serializable
def get_days_names(locale=locale.getdefaultlocale(), short=False):
    """
        Un dictionnaire contenant un mapping entre les numéros des jours
        de semaine et leurs noms selon la locale donnée.
    """

    return {i: get_day_name(i, locale) for i in xrange(7)}

En usage ordinaire, la fonction retourne bien ce qui est prévu :

>>> get_days_names()
{0: 'lundi', 1: 'mardi', 2: 'mercredi', 3: 'jeudi', 4: 'vendredi', 5: 'samedi', 6: 'dimanche'}
>>> get_days_names(locale=('en_US', 'UTF-8'))
{0: 'Monday', 1: 'Tuesday', 2: 'Wednesday', 3: 'Thursday', 4: 'Friday', 5: 'Saturday', 6: 'Sunday'}

Mais on peut choisir le format à la sortie :

>>> get_days_names.as_json()
'{"0": "lundi", "1": "mardi", "2": "mercredi", "3": "jeudi", "4": "vendredi", "5": "samedi", "6": "dimanche"}'
>>> get_days_names.as_pickle(locale=('en_US', 'UTF-8'))
"(dp0\nI0\nS'Monday'\np1\nsI1\nS'Tuesday'\np2\nsI2\nS'Wednesday'\np3\nsI3\nS'Thursday'\np4\nsI4\nS'Friday'\np5\nsI5\nS'Saturday'\np6\nsI6\nS'Sunday'\np7\ns."

Ici, on a attacher une fonction à une autre fonction, en mettant la deuxième dans un attribut de la première.

Comme les décorateurs sont des fonctions, rien ne vous empêche de faire pareil avec un décorateur, et c’est de cette manière que @property attache un décorateur setter à chaque méthode.



Télécharger le code des articles

]]>
http://sametmax.com/creer-un-decorateur-a-la-volee/feed/ 7 6624
Batbelt, la lib des petits outils Python qui vont bien http://sametmax.com/batbelt-la-lib-des-petits-outils-python-qui-vont-bien/ http://sametmax.com/batbelt-la-lib-des-petits-outils-python-qui-vont-bien/#comments Mon, 03 Jun 2013 08:57:33 +0000 http://sametmax.com/?p=6327 A force de coder plein de projets, il y a des opérations qui reviennent très souvent. Ces traitements sont petits et complètement sans relation, difficile d’en faire quelque chose. J’ai tout de même finit par en faire un lib, batbelt, qui au final n’est qu’une grosse collections de snippets que j’utilise régulièrement. Il y a aussi des trucs que j’utilise moins ou des astuces / hacks un peu crades, c’est toujours pratique pour geeker à l’arrache vite fait. Vous allez d’ailleurs retrouver des bouts de code dont j’ai déjà parlé sur le site

pip install batbelt

Et la plupart des fonctions sont accessible avec un from batbelt import...

Voici les choses qui pourraient vous intéresser le plus dans batbelt…

To timestamp

Mais combien de fois j’ai du la coder celle-là ? En plus l’inverse existe, alors pourquoi, mon Dieu, pourquoi ?

>>> from datetime import datetime
>>> to_timestamp(datetime(2000, 1, 1, 2, 1, 1))
946692061
>>> datetime.fromtimestamp(946688461) # tu as codé celle là et pas l'autre connard !
datetime.datetime(2000, 1, 1, 2, 1, 1)

Récupérer une valeur dans une structure de données imbriquée

Au lieu de faire :

try:
    res = data['cle'][0]['autre cle'][1]
except (KeyError, IndexError):
    res = "valeur"
    

On peut faire :

get(data, 'cle', 0, 'autre cle', 1, default="valeur")

Récupérer la valeur d’un attribut dans un attribut dans un attribut…

Pareil, mais pour les attributs.

try:
    devise = voiture.moteur.prix.devise
except AttributeError:
    devise = "euro"

On peut faire :

devise = attr(voiture, 'moteur', 'prix', 'devise', default='euro')

Itérons, mon bon

Ces fonctions retournent des générateurs qui permettent d’itérer par morceau ou par fenêtre glissante.

>>> for chunk in chunks(l, 3):
...     print list(chunk)
...
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
>>> for slide in window(l, 3):
...     print list(slide)
...
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]

Ça devrait être en standard dans Python.

Parfois on veut juste le premier élément d’une collection. Ou juste le premier à être vrai:

>>> first(xrange(10))
0
>>> first_true(xrange(10))
1

Marche avec n’importe quel itérable, contrairement à [0] qui ne marche que sur les indexables. Et en prime on peut spécifier une valeur par défaut:

>>> first([], default="What the one thing we say to the God of Death ?")
'What the one thing we say to the God of Death ?'

Set ordonné

On a des dicts ordonnés dans la lib standard, mais pas de set ordonné. On en a pas besoin souvent, mais ça peut être TRES pratique, et TRES chiant à implémenter soi-même.

Donc acte.

>>> for x in set((3, 2, 2, 2, 1, 2)): # booooooo
...     print x
...
1
2
3
>>> for x in sset((3, 2, 2, 2, 1, 2)): # clap clap !
...     print x
...
3
2
1

Attention, c’est pas la structure de données la plus rapide du monde…

Je suis une feignasse et j’aime les one-liners sur les dicos

Je ne comprends pas pourquoi + ne fonctionne pas sur les dico.

>>> dmerge({"a": 1, "b": 2}, {"b": 2, "c": 3})
{'a': 1, 'c': 3, 'b': 2}

Ne modifie pas les dictionnaires originaux.

>>> from batbelt.structs import rename
>>> rename({"a": 1, "b": 2})
>>> rename({"a": 1, "b": 2}, 'b', 'z')
{u'a': 1, u'z': 2}

Modifie le dictionnaire original et n’est PAS thread safe.

Et le cas tordu mais tellement satisfaisant :

>>> from batbelt.structs import unpack
>>> dct = {'a': 2, 'b': 4, 'z': 42}
>>> a, b, c = unpack(dct, 'a', 'b', 'c', default=1)
>>> a
2
>>> b
4
>>> c
1

Slugifier

>>> slugify(u"Hélo Whorde")
helo-whorde

Il y a pas mal de réglages possibles avec slugify(), mais je vous laisse les découvrir :-) Cette fonction fait partie du sous-module strings, qui contient d’autres utilitaires comme escape_html/unescape_html (qui transforme les caractères spéciaux en HTML entities et inversement) ou json_dumps/json_loads (qui fait un dump / load du JSON en prenant en compte le type datetime).

Importer une classe ou une fonction depuis une string

Dès que vous faites un fichier de config vous avez besoin de ce genre de truc, mais la fonction __import__ a une signature uber-zarb. Voici une version beaucoup plus simple:

TaClasse = import_from_path('foo.bar.TaClasse')
ton_obj = TaClasse()

Capturer les prints

Parfois on a une lib qui print plutôt que de retourner une valeur. C’est très chiant. J’ai donc fait un context manager qui permet de récupérer tout ce qui est printé dans le block du with.

>>> with capture_ouput() as (stdout, stderr):
...    print "hello",
...
>>> print stdout.read()
hello

Créer un décorateur qui accepte des arguments

Même dans le cas où vous avez parfaitement compris les décorateurs grâce à un très bon tuto (^^), se souvenir de comment faire un décorateur qui attend des arguments en paramètre, c’est mission impossible. Voici donc un décorateur… pour créer un décorateur.

Étape un, écrire votre décorateur :

# tout les arguments après 'func' sont ceux que votre décorateur acceptera
@decorator_with_args()
def votre_decorateur(func, arg1, arg2=None):

    if arg1:
        # faire un truc

    # ici on fait juste le truc habituel des décorateurs
    # wrapper, appel de la fonction wrappée et retour du wrapper...
    def wrapper():
        # arg2 est dans une closure, on peut donc l'utiliser dans
        # la fonction appelée
        return func(arg2)


    return wrapper

Et on peut utiliser son décorateur tranquile-bilou :

@votre_decorateur(False, 1)
def hop(un_arg):
    # faire un truc dans la fonction décorée

Les processus parallèles finissent toujours par se rencontrer à l’infini et corrompre leurs données

Mais en attendant on en a quand même besoin. Parfois un petit worker, c’est sympa, pas besoin de faire compliqué et de sortir des libs de task queue complètes:


from batbelt.parallel import worker

@worker()
def une_tache(arg):
    # faire un truc avec arg
    arg = arg + 10
    return arg


# on demarre le worker
process = une_tache.start()

# on balance des tâches au worker
for x in range(10):
    process.put(x)

# on récupère les résultats (optionnel)
# ca peut être dans un fichier différent
for x in range(10):
    print process.get()

## 10
## 11
## 12
## 13
## 14
## 15
## 16
## 17
## 18
## 19

# on arrête le worker
process.stop()

Le worker est un subprocess par défaut, mais vous pouvez en faire un thread avec @worker(method=”tread”). Toujours utile, par exemple pour avec un processeur de mails qui envoit tous les mails hors du cycle requête / réponse de votre site Web. Par contre si votre process meurt la queue est perdue.

Template du pauvre

Avec format(), on a déjà un mini-langage de template intégré. Pas de boucle, mais pour des tâches simples ça suffit. Du coup j’ai une fonction render() qui prend un fichier de template au format string Python et qui écrit le résultat dans un autre. Pratique pour faire des fichiers de conf configurable.

from batbelt.strings import render

render('truc.conf.tpl', {"var": "value"}, "/etc/truc.conf")

Il y a aussi des implémentations de Singleton, du Null Pattern, etc. Mais ça s’utilise moins souvent alors je vais pas faire une tartine.

]]>
http://sametmax.com/batbelt-la-lib-des-petits-outils-python-qui-vont-bien/feed/ 15 6327
Le pattern observer en utilisant des décorateurs http://sametmax.com/le-pattern-observer-en-utilisant-des-decorateurs/ http://sametmax.com/le-pattern-observer-en-utilisant-des-decorateurs/#comments Sun, 14 Oct 2012 15:03:07 +0000 http://sametmax.com/?p=2591 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.

]]>
http://sametmax.com/le-pattern-observer-en-utilisant-des-decorateurs/feed/ 7 2591
Les context managers et le mot clé with en Python http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/ http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/#comments Mon, 03 Sep 2012 17:56:43 +0000 http://sametmax.com/?p=1987 with est utilisé comme dans aucun autre langage en Python. Au premier abord mystérieux, il agit en fait comme les décorateurs en permettant d'exécuter du code automatiquement avant et après un autre code. Mais à l'image des décorateurs, tout ce qu'il fait pourrait être écrit à la main sans utiliser le mot clé with. Utiliser with est une question de style. ]]> Le mot clé with est utilisé comme dans aucun autre langage en Python. Au premier abord mystérieux, il agit en fait comme les décorateurs en permettant d’exécuter du code automatiquement avant et après un autre code. Mais à l’image des décorateurs, tout ce qu’il fait pourrait être écrit à la main sans utiliser le mot clé with. Utiliser with est une question de style.

Supposons que vous vouliez afficher quelque chose avant un bout de code, et après un bout de code, même si celui-ci rate. Vous feriez quelque chose comme ça:

def truc():
    print "machin"

print "Avant"
try:
    truc()
finally:
    print "Après"

Et ça va afficher:

Avant
machin
Après

Et avec:

def truc():
    print "machin"
    raise Exception('Fail !')

‘Après’ sera quand même affiché. Ça plantera, mais la dernière action sera toujours faite.

Si vous le faites souvent, vous voudrez factoriser du code. Un des moyens de le faire est d’utiliser les context managers.

Créer son propre context manager

Un context manager est une classe ordinaire en Python. Sa seule spécificité est de déclarer une méthode __enter__() et une méthode __exit__(). Ces méthodes sont des méthodes ordinaires, leur nom spécial est juste là par convention, et en les nommant ainsi on s’assure qu’elles seront détectées et utilisées automatiquement.

Notre code là haut peut donc se réécrire ainsi:

class MonSuperContextManager(object):
    def __enter__(self):
        print "Avant"
    def __exit__(self, type, value, traceback):
        # faites pas attention aux paramètres, ce sont toutes les infos
        # automatiquement passées à __exit__ et qui servent pour inspecter
        # une éventuelle exception
        print "Après"

with MonSuperContextManager():
    truc()

L’avantage de with est multiple:

  • Il permet de visualiser très précisément où on entre dans l’action et où on en sort (c’est un seul block)
  • Il permet de réutiliser les actions faite à l’entrée et à la sortie de l’action.
  • Même si une exception est levée, l’action de sortie sera exécutée juste avant le plantage. __exit__ est en effet garantie d’être appelée quoiqu’il arrive. Bon, évidement, si il y a une coupure de courant…

En gros, créer un context manager, c’est faire un raccourci lisible pour try/finally. Point.

Un exemple utile de context manager

Supposons que vous ayez beaucoup de travail à faire dans plein de dossiers. Vous voulez vous assurer que vous allez dans le dossier de travail, puis que vous retournez au dossier initial à chaque fois.

import os

class Cd(objet):
    def __init__(dirname):
        self.dirname = dirname
    def __enter__(self):
        self.curdir = os.getcwd()
        os.chdir(self.dirname)
    def __exit__(self, type, value, traceback):
        os.chdir(self.curdir)

On l’utilise comme ça:

# ici on est dans /home/moi

with Cd('/'):

    # faire un truc dans /

    with Cd('/opt'):

        # faire un truc dans /opt

    # ici on est dans /

# ici on est dans /home/moi

C’est d’ailleurs ce que fait fabric.

Le mot clé as

Tout ce qu’on retourne dans __enter__ peut être récupéré grâce au mot clé as. Imaginons un context manager qui permette d’ouvrir un fichier et de le fermer automatiquement:

class OpenFile(objet):
    def __init__(filename, mode='r'):
        self.filename = filename
        self.mode = mode
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        # ici on retourne l'objet fichier, il sera accessible avec "as"
        return self.file
    def __exit__(self, type, value, traceback):
        self.file.close()

On l’utilise comme ceci:

with OpenFile('/etc/fstab') as f:
    for line in f:
        print line

f va contenir ici l’objet fichier, car nous l’avons retourné dans __enter__. A la fin du bloc with, le fichier sera fermé automatiquement.

Et devinez quoi, Python possède déjà un context manager qui fait ça:.

with open(vot_fichier_msieu_dames) as f:
   # faire un truc

Context managers sous forme de fonctions

Faire les choses sous forme de classes, c’est pratique quand on a beaucoup de logique à encapsuler. Mais la plupart des context managers sont très simples. Pour cette raison, Python vient avec plein d’outils pour se simplifier la vie avec with dans un module judicieusement nommé contextlib.

Pour l’utiliser, il faut avoir des notions sur les décorateurs, et le mot clé yield. Si ce n’est pas votre cas, restez sur la version sous forme de classe :-)

Supposons que l’on veuille recréer le context manager open:

from contextlib import contextmanager

@contextmanager
def open(filename, mode):
    try:
        f = open(filename, mode)
        yield f
    finally:
        f.close()

Bon, c’est simplifié, hein, le vrai est plus robuste que ça.

Comment ça marche ?

D’abord, on utilise le décorateur @contextmanager pour dire à Python que la fonction sera un context manager.

Ensuite, on fait un try/finally (il est pas automatique comme avec __enter__ et __exit__).

yield sépare le code en deux: tout ce qui est avant est l’équivalent de __enter__, tout ce qui est après est l’équivalent de __exit__. Ce qui est “yieldé” est ce que l’on récupère avec le mot clé as.

Context manager et décorateur, le shampoing deux en un

Ces deux fonctionnalités se ressemblent beaucoup: elles permettent toutes les deux de lancer du code automatiquement avant et après un code tiers. La seule différence est que le context manager le fait à la demande, alors que le décorateur s’applique à la définition d’une fonction.

Quand on sait comment ils marchent, il est facile de faire un context manager utilisable également en tant que décorateur.

from functools import wraps

class ContextDecorator(object):
    # __call__ est une méthode magique appelée quand on utilise () sur un objet
    def __call__(self, f):
        # bon, cette partie là suppose que vous savez comment marche un
        # décorateur, si c'est pas le cas, retournez lire l'article sur S&M
        # linké dans le premier paragraphe
        @wraps(f)
        def decorated(*args, **kwds):
            # notez le with appelé sur soi-même, c'est y pas mignon !
            with self:
                return f(*args, **kwds)
        return decorated

Et voilà, il suffit d’hériter de ça, et on a un décorateur + context manager. Par exemple, si on veut timer un truc:

import datetime

class TimeIt(ContextDecorator):

    def __enter__(self):
        self.start = datetime.datetime.now()
        print self.start

    def __exit__(self, type, value, traceback):
        print (datetime.datetime.now() -self.start).total_seconds()

Timer juste un appel:

def foo():
    # faire un truc

with TimeIt():
    foo()

Timer tous les appels:

@TimeIt()
def foo():
   # faire un truc

Notez que ContextDecorator est présent par défaut dans le module contextlib sous Python 3.2.

]]>
http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/feed/ 20 1987
Explication de code: des mixins et des décorateurs de méthode pour Django http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/ http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/#comments Tue, 21 Aug 2012 14:37:57 +0000 http://sametmax.com/?p=1812 Suite à notre appel à l’envoi de code à expliquer, nous avons reçu ceci:

Pouvez-vous m’aider à comprendre ce code?

https://gist.github.com/3092600

La fonction dispatch, à quoi sert-elle?

L’appel via super, on appelle le parent de la classe?

Merci, je vous ferai une statue un jour!

Prépare le marbre.

Programmation orientée objet

Commençons par:

L’appel via super, on appelle le parent de la classe?

Oui, très exactement.

Et j’en profite pour rappeler que si vous utilisez les CBV, il va vous falloir vous toucher en programmation orientée objet: définition de classe, héritage multiple, overriding, et autres joyeusetés.

Comme ce genre d’infos peut faire l’objet d’une série d’articles à part entière, je vais être obligé de partir du principe que vous savez coder en objet pour expliquer ce code. Sinon, ce post ferait quelques milliers de lignes. Si ce n’est pas votre cas, lisez notre dossier sur la POO, et revenez après.

Worflow des CBV

La fonction dispatch, à quoi sert-elle?

Les CBV ont un ordre d’éxécution pour leurs méthodes: render_to_response() retourne la vue, mais elle est appelée depuis get() ou post(), qui appellent aussi get_context_data() pour créer le context du template. Et get_context_data() appelle get_query_set() pour les vues qui utilisent l’ORM. Il faudrait que je fasse un gros schéma de tout ça un jour.

Dispatch() est la méthode qui appelle toutes les autres. Elle choisi notament si on appelle la méthode get() ou post(). C’est en quelque sorte la méthode mère. Si on veut faire un truc avant que la vue ne travaille, c’est la dedans qu’il faut agir.

Les décorateurs de méthode

Comme vous l’avez vu, le code utilise @method_decorator. Encore une fois, je vais partir du principe que vous vous touchez avec les décorateurs, si ce n’est pas le cas, il y a un article pour ça™.

Néanmoins, dans l’article on ne parle que des décorateurs de fonctions. Comment décorer une méthode ? Et bien c’est pareil, sauf qu’il faut que votre décorateur accepte self en plus comme argument dans la fonction qu’il retourne.

Afin d’éviter de réécrire tous les décorateurs en double, on utilise @method_decorator qui est un décorateur… pour décorateurs ^^ Il transforme un décorateur pour fonction afin qu’il soit applicable à une méthode.

Ainsi quand vous voyez @method_decorator(login_required), ça veut dire “tranformer le décorateur login_required pour qu’il marche sur les méthodes, et l’apppliquer sur la méthode juste en dessous”

Les mixins

Je suis sûr que vous n’avez pas pu vous empêcher de vous demander, à la vue de ça:

LoginRequiredMixin(object):
    """
        View mixin which requires that the user is authenticated.
    """

Mais c’est quoi un mixin non de diou ?

Alors, déjà, on se rassure, ce n’est pas encore un nouveau truc compliqué à apprendre, c’est juste le nom d’un truc que vous connaissez déjà.

Un mixin, c’est le nom qu’on donne à une classe dont le but est exclusivement d’être utilisé pour l’héritage, afin de rajouter une fonctionalité à la classe enfant.

En gros, ce n’est qu’un nom, ça n’a rien de spécial, c’est une classe normale. Mixin est juste le rôle de la classe.

Les mixins ne sont PAS des interfaces, car ils font toujours quelque chose. Ils sont utilisés quand on a un code générique qu’on veut réutiliser dans plusieurs classes.

Les mixins changent le comportement, mais PAS la nature de la classe enfant. Il n’y a pas de AnimalMixing dont hériterait une class Chien, ou Chat. Ca c’est de l’héritage normal. On ne parle de mixin que pour le comportement.

Les mixins ne marchent que dans les langages qui autorisent l’héritage multiple, car on doit pouvoir hériter de plein de mixins d’un coup pour que ça soit utile.

Explication du premier Snippet

Vous vous souvenez du temps où c’était si simple de protéger une vue ?

@login_required
def ma_vue(request):

Avec les CBV, ce temps là est finit mes amis. C’est pour ça que je n’ai pas beaucoup d’amour pour elles.

Pour protéger une CBV on a plusieurs choix:

class MaView(ListView):
    ...

ma_vue = login_required(MaView.as_view())

Et on importe ma_vue dans urls.py.

OU directement dans urls.py:

...
url('/this/is/not/a/url', login_required(MaView.as_view()))
...

OU la méthode recommandée par les mecs de Django, méga simple et intuitive:

class MaView(ListView):
    ...
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(ProtectedView, self).dispatch(*args, **kwargs)

On applique le décorateur @login_required à la méthode dispatch(). Comme c’est la méthode qui appelle toutes les autres, la vue est ainsi protégée.

Seulement voilà, tout celà est bien relou, et l’auteur des snippets y remédie:

class LoginRequiredMixin(object):
    """
      View mixin which requires that the user is authenticated.
    """
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        return super(LoginRequiredMixin, self).dispatch(
            self, request, *args, **kwargs)

Ce faisant, il créé un mixin qui va appliquer ce décorateur. Mixin qu’on peut réutiliser ainsi:

class MaView(LoginRequiredMixin, ListView):
    ...

Et protéger une vue redevient simple à nouveau !

Attention, il faut bien mettre le mixin en premier dans la liste de parents. En effet, dispatch() de MaView va appeler celui de LoginRequiredMixin qui va appeler super() qui va va ainsi appeler dispatch() de ListView. C’est ce qu’on appelle le MRO (Method Résolution Order), l’ordre à vachement d’importance, et ça mériterait un article à lui tout seul.

Je résume:

dispatch() est la méthode d’un vue qui appelle toutes les autres. LoginRequiredMixin a sa méthode dispatch() protégée pour qu’elle ne soit accessible que par les utilisateurs enregistrés. MaView hérite du mixin pour intégrer cette fonctionalité. Du coup son dispatch() va utiliser le dispatch() de LoginRequiredMixin, qui est protégé. Comme LoginRequiredMixin est bien faites, son dispatch() appelle celui de ListView, et ainsi la vue fonctionne correctement.

Les autres snippets

On peut maintenant aller plus vite.

Le deuxième mixin permet de n’autoriser l’accès à une vue que si on a les permissions nécessaires (Django vient en effet avec toute une gestion des permissions dans son app contrib ‘auth’)

class PermissionsRequiredMixin(object):
    # les permissions nécessaires sont stockées dans cet attribut
    required_permissions = ()

    # dispatch est encore une fois protégée contre les users non loggés
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # on rajoute une subtilité:
        # on vérifie si le user a les permissions exigées avant
        # d'appler le dispatch() du parent
        # si on a pas les permissions, on rajoute un message d'erreur
        # et on redirige vers la page de login
        # sinon on appelle le dispatch() du parent comme d'hab
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Ca s’utilise comme ça:

class MaVue(PermissionsRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Un utilisateur qui n’a pas les permissions create_cocaine et delete_cocaine de l’app ‘dealer’ sera redirigé vers la page de login.

En effet, MaVue.dispatch appelle la méthode PermissionsRequiredMixin.dispatch, mais avec une subtilité: le self passé en paramètre est celui de MaVue. self.required_permissions dans PermissionsRequiredMixin.dispatch est donc en vérité MaVue.required_permissions. Relisez ce paragraphe plusieurs fois.

Notez en revanche que la page de redirection n’est pas configurable, ce qui est bien dommage. Ca se corrige facilement:

class PermissionsRequiredMixin(object):
    required_permissions = ()
    redirect_url = settings.LOGIN_URL # <== HOP

    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perms(self.required_permissions):
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(self.redirect_url) # <== HOP
        return super(PermissionsRequiredMixin, self).dispatch(
            request, *args, **kwargs)

Du coup on a plus de marge de manoeuvre.

Le 3eme et 4eme snippet, c'est la même chose. Kiff kiff. Pareil.

Mais au lieu de vérifier les permissions, on vérifie juste si l'utilisateur à accès à l'admin Django (is_staff) ou que l'utilisateur est un superutilisateur (is_superuser: il a toutes les permissions):

class StaffRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # j'ai pas besoin de vous expliquer ça quand même ?
        if not request.user.is_staff:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(StaffRequiredMixin, self).dispatch(request,
            *args, **kwargs)

class SuperUserRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwargs):
        # je pense que c'est assez explicite
        if not request.user.is_superuser:
            messages.error(
                request,
                'You do not have the permission required to perform the '
                'requested operation.')
            return redirect(settings.LOGIN_URL)
        return super(SuperUserRequiredMixin, self).dispatch(request,
            *args, **kwargs)

Comme je le disais plus haut, on peut utiliser plusieurs mixins en même temps. Par exemple, si je veux limiter l'accès à une vue aux dealers qui font partie de mon staff:

class MaVue(PermissionsRequiredMixin, StaffRequiredMixin, ListView):
    required_permissions = (
        'dealer.create_cocaine',
        'dealer.delete_cocaine',
    )

Notez encore une fois que les mixins sont là en premier, car MaVue.dispatch appelle PermissionsRequiredMixin.dispatch qui appelle StaffRequiredMixin.dispatch qui appelle ListView.dispatch.

En revanche, dans ce cas précis l'ordre des mixins, entre eux, n'a pas d'importance. Ce ne sera pas toujours le cas: il faut comprendre comment les mixins agissent pour savoir si certains doivent être mis en premier. C'est tout le problème des CBV: pour en faire un usage productif, la somme de choses à savoir est assez démente.

]]>
http://sametmax.com/explication-de-code-des-mixins-et-des-decorateurs-de-methode-pour-django/feed/ 24 1812
Mémoization d’une fonction Python http://sametmax.com/memoization-dune-fonction-python/ http://sametmax.com/memoization-dune-fonction-python/#comments Fri, 20 Jul 2012 20:37:27 +0000 http://sametmax.com/?p=1223 mémoization est une forme de mise en cache, elle consiste à cacher le résultat d'une fonction afin que les appels successifs avec des paramètres identiques utilisent le cache plutôt que de calculer à nouveau les données.]]> La mémoization est une forme de mise en cache, elle consiste à cacher le résultat d’une fonction afin que les appels successifs avec des paramètres identiques utilisent le cache plutôt que de calculer à nouveau les données.

En gros:

>>> fonction_super_lente(True, '127.0.0.1') # calcule
>>> fonction_super_lente(True, '127.0.0.1') # ne calcule pas, retourne le cache
>>> fonction_super_lente(True, '192.168.0.1') # calcule car les paramètres sont différents

En Python on peut le faire de plusieurs manières, malgré ce que prétend la philosophie du langage.

La version avec dico externe

resultats = {}
def fonction_super_lente(le_faire, sur_quoi):
    
    if not (le_faire, sur_quoi) in resultats:
        resultats[(le_faire, sur_quoi)] = # balancer le calcul lent ici

    return resultats[(le_faire, sur_quoi)]

Cela marche parceque resultats est gardé dans une closure et donc accessible dans la fonction. Comme il est défini avant, et que les dictionnaires sont mutables, on a toujours la même référence au même dico à chaque appel. On a juste a vérifier qu’un résultat pour ces param existent (les tuples peuvent être des clés de dictionnaires), et si non, on remplit le cache.

Avantages:

  • c’est facile à comprendre;
  • on peut manipuler le cache depuis l’extérieur.

Inconvénient:

  • le namespace du module est pollué;
  • on risque d’importer le cache par erreur.

La version avec paramètre additionel

def fonction_super_lente(le_faire, sur_quoi, _resultats={}):
    
    if not (le_faire, sur_quoi) in resultats:
        _resultats[(le_faire, sur_quoi)] = # balancer le calcul lent ici

    return _resultats[(le_faire, sur_quoi)]

Même chose que précédement, mais la différence est que le cache est stocké dans un paramètre. Cela marche car les paramètres sont initialisés à la déclaration en Python, et une seule fois.

Notez le underscore devant le nom de paramètre qui est une convention pour désigner un paramètre qui ne fait pas partie de l’API publique.

Avantages:

  • c’est encore pas trop dur à comprendre;
  • le namespace est clean.

Inconvénient:

  • la signature de la fonction possède un argument artificiel.

La version avec attribut

def fonction_super_lente(le_faire, sur_quoi):
    
    resultats = getattr(function_super_lente, '_resultats', {})
    if not (le_faire, sur_quoi) in resultats:
        resultats[(le_faire, sur_quoi)] = # balancer le calcul lent ici
        function_super_lente._resultats = resultats

    return resultats[(le_faire, sur_quoi)]

Cela marche car les fonctions en Python sont des objets. On peut leur rajouter des attributs à la volées comme pour n’importe quel objet :-)

Notez l’usage du troisième paramètre de getattr() qui nous permet d’avoir une valeur par défaut même si l’attribut n’existe pas encore.

Avantages:

  • pas de polution du namespace ni de la signature.

Inconvénient:

  • votre collègue va surement vous demander: WTF ?

La version avec décorateur

Il existe pas mal de versions de décorateurs de mémoization disponibles sur le Net. Ca s’utilise généralement comme ça :

from malib import memoized

@memoized
def fonction_super_lente(le_faire, sur_quoi):
    
    return # le truc normalement

Avantages:

  • y a pas plus propre;
  • y a pas plus facile.

Inconvénient:

  • une dépendance de plus;
  • chaque implémentation possède une caractéristique (souvent une limite): il faut donc comprendre des codes parfois complexes pour les utiliser en toute tranquilité d’esprit.

Petit rappel

Cette technique, de par sa nature, implique de tout stocker en mémoire vive. Donc attention à votre RAM, et vérifiez bien que ça vaut le coup d’échanger la charge CPU contre celle de vos barettes. Ensuite, le bénéfice est d’autant plus grand que le code tourne longtemps. Si c’est un script lancé de nombreuses fois, avec quelques appels à la fonction, le gain est faible: entre chaque initialisation de la VM Python, le cache disparait. Dans ce cas il vaut mieux se tourner vers une solution telle que Redis.

Le cas typique d’usage pertinent est celui d’un daemon faisant des appels à une API WEB: en cachant les résultats, vous économisez une requête HTTP.

]]>
http://sametmax.com/memoization-dune-fonction-python/feed/ 11 1223