Créer un décorateur à la volée


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
<built-in method setter of property object at 0x1a1baf8>

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

7 thoughts on “Créer un décorateur à la volée

  • foxmask

    pourquoi :

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

    et pas ?

    func.as_json = lambda **k: json.dumps(func(**k))
  • Sam Post author

    L’interêt d’un décorateur, c’est qu’on peut l’ajouter et le retirer à loisir, et sur plusieurs fonctions.

    Si on utilise juste **k, on ne peut plus utiliser le decorateur sur des fonctions avec des paramètres positionnels :

    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 **k: json.dumps(func(**k))
        func.as_pickle = lambda **k: pickle.dumps(func(**k))
     
        return func
     
    @serializable
    def get_day_name(day_number, locale, short=False):
        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
     
    print get_day_name.as_json(0, ('fr_FR', 'UTF-8'))
     
    Traceback (most recent call last):
      File "/home/sam/Bureau/functions_as_functions_attributes.py", line 103, in <module>
        print get_day_name.as_json(0, ('fr_FR', 'UTF-8'))
    TypeError: <lambda>() takes exactly 0 arguments (2 given)
  • groug

    Salut Sam & Max ! J’ai découvert votre blog en début d’année, et ma connaissance de Python est un gros gruyère. J’estime qu’il y a plein de choses subtiles que je ne connais pas du langage, et c’est souvent que j’en découvre grâce à votre blog en me disant à chaque fois : p’tain, c’est quand même de la balle, Python. Aujourd’hui ne fait pas exception, alors j’en profite pour une fois pour vous dire un grand merci !

    Mais j’ai une question : décorateur différent ou pas, j’étais persuadé qu’il n’était pas possible de créer 2 méthodes du même nom, même si le nombre d’arguments différait. Comment se fait-ce ?

  • Sam Post author

    En fait, on ne créer pas deux méthodes de même nom, car en Python on ne donne pas de nom aux méthodes. Sous le capot, def attache simplement la référence à la méthode à un nom. Quand la seconde méthode est crée, elle est attachée à ce nom et l’ancienne n’est plus attachée à ce nom, mais ici ne disparaît pas pour autant.

    En effet, un objet en Python n’est supprimé que dans le cas où aucune référence ne pointe vers lui. Dans notre cas précis, @property garde une référence interne de la méthode et elle n’est pas supprimée.

    Pour aller plus loin dans l’explication : @property va créer un descripteur qu’il va placer en lieu et place de l’attribut portant le nom des méthodes. Il va ensuite attacher chaque méthode à ce descripteur. Au final, quand on lit, écrit ou supprime le contenu de l’attribut, on appelle le descripteur, qui lui appelle les méthodes.

    Le descripteur a donc une référence pour chaque méthode décorée, ainsi les méthodes ne sont jamais supprimées, et reste accessibles.

    Mangez des pommes !

  • groug

    Ok, c’est la non-disparition que je n’avais pas comprise. Merci !

    Ce que je ne comprends pas (mais que j’accepte), c’est pourquoi, alors que le décorateur @valeur.setter mentionne le nom de la property (valeur), la méthode doit être nommée valeur aussi. Si je change le nom :

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

    Python me dit qu’il ne trouve pas la méthode valeur_set quand je fais bille.valeur = "foo". Ca paraît un peu redondant. Ca paraîtrait, dans ce cas, plus logique d’avoir un décorateur de nom @property.setter, si c’est pour devoir, de toute façon, nommer la méthode intelligemment.

  • Sam Post author

    Je me suis fait la même remarque, il faudrait regarder dans le code source pour trouver la réponse, mais j’ai la flemme.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.