decorators – 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 Refactoriser ses vérifications en Python http://sametmax.com/refactoriser-ses-verifications-en-python/ http://sametmax.com/refactoriser-ses-verifications-en-python/#comments Sun, 23 Jun 2013 21:56:00 +0000 http://sametmax.com/?p=6437 On nous a interpelé sur Twitter pour nous demander comment faire un code comme ceci (en tout cas similaire) plus propre:

# -*- coding: utf-8 -*

from __future__ import unicode_literals

import random

def verification1(val):
    return bool(random.randint(0, 1))

verification2 = verification1
verification3 = verification1

def function(valeur):

    if not verification1(valeur):
        print("Erreur, la vérification 1 n'est pas passée")

    if not verification2(valeur):
        print("Erreur, la vérification 2 n'est pas passée")

    if not verification3(valeur):
        print("Erreur, la vérification 3 n'est pas passée")

    return valeur

Ceci est évidement un exemple tout bidon, mais la problématique, c’est qu’on a plein de vérifications à faire, et un code à lancer si jamais ces vérifications échouent. Et chaîner les if, c’est vrai que c’est pas super mignon (même si c’est tout à fait valide, faut pas non plus se prendre trop la tête avec ça).

En Python, on a plusieurs manières de s’occuper de ça. La première, j’en ai déjà parlé, c’est l’injection de dépendances. On va passer ces checks en paramètres.

Car je le rappelle, on peut passer des fonctions en paramètres, on peut mettre des fonctions dans des listes, et on peut même passer des listes de fonctions en paramètres. Ce qui ici va nous permettre de :

  • Regrouper les traitements.
  • Permettre de changer les traitements à la volée.
  • Avoir malgré tout un traitement par défaut.
  • Rendre les groupes de vérifications réutilisables


VERIF = (
    (verification1, "Erreur, la vérification 1 n'est pas passée"),
    (verification2, "Erreur, la vérification 2 n'est pas passée"),
    (verification3, "Erreur, la vérification 3 n'est pas passée"),
    ((lambda v: not bool(random.randint(0, 1))),
    "Erreur, la vérification 4 n'est pas passée")
)

def faire_verifications(verifications):
    for verif, msg in verifications:
        if not verif(valeur):
            print msg

def function(valeur, verifications=VERIF):
    faire_verifications(verifications)
    return valeur

Cette manière de faire est plus verbeuse si votre liste de if est courte, ou si vous n’avez à le faire que pour une fonction. Mais elle devient vite plus courte et clair dans le cas où votre code grandi.

Elle aussi l’avantage de pouvoir insérer ou retirer une modification très simplement (même avec une simple lambda). Enfin, le code de vérification est découplé du code de la fonction ce qui ajoute les bénéfices suivant :

  • Votre code de vérification peut être déplacé dans un module dédié.
  • Si vous avez des vérifications en plusieurs endroits, il n’y a qu’un seul endroit pour corriger les bugs des vérifications, et un seul pour faire une modification des vérifications.
  • Le code de la fonction redevient simple : elle fait une vérification (on s’en branle de comment ça marche, on sait que ça vérifie), et ensuite elle s’occupe de SON boulot. C’est la partie du code qui nous intéresse quand on va voir la fonction.

On peut évidement imaginer autre chose qu’un print: lever une exception, faire un log, ou même carrément aussi passer une fonction en lieu et place du message d’erreur qui fait office de callback quand la vérification échoue (et qui par défaut print, le meilleur des deux mondes).

Si vos vérifications deviennent très courantes, alors, l’utilisation de décorateurs prend tout son sens. Je ne vais pas rentrer sur comment écrire son décorateur, il y a un autre article pour ça, mais on peut obtenir un résultat du genre :

def verif(verification, message):
    def decorateur(func):
        def wrapper(*args, **kwargs):
            if not verification(*args, **kwargs):
                print message
            return func(*args, **kwargs)
        return wrapper
    return decorateur

# si on utilise souvent une vérif, on peut l'avoir 
# tout le temps tout la main
verif3 = verif(verification3, "Erreur, la vérification 3 n'est pas passée")

# et ensuite il suffit d'appliquer autant de décorateur qu'on le souhaite
@verif(lambda v: not bool(random.randint(0, 1)), "Erreur, la vérification 4 n'est pas passée")
@verif3
@verif(verification2, "Erreur, la vérification 2 n'est pas passée")
@verif(verification1, "Erreur, la vérification 1 n'est pas passée")
def function(valeur):
    return valeur

Les avantages ici sont :

  • Corps de la fonction initial vide de toute vérification. Les vérifications sont purement déclaratives (comme par exemple la vérification de login dans Django).
  • Chaque vérif est très facilement réutilisable, il suffit de mettre le décorateur. Si on utilise une vérif très souvent, alors on peut même mettre le décorateur dans une variable et juste déclarer @ma_verif.
  • C’est très pythonique comme style.

En résumé : si vous avez un petit code, ne vous prenez pas la tête. Une série de if est tout à fait valide, ça marche bien, c’est clair et lisible. Il faut savoir aller au plus simple. Mais si votre code devient touffu, utiliser une liste de fonction peut aider à faire le ménage. Si vous commencer carrément à avoir un gros projet ou que vous codez une lib réutilisable, les décorateurs seront de précieux alliés : ils permettent à celui qui passe derrière vous de se foutre complètement de savoir comment ça marche et de juste assembler son code comme un légo.


Télécharger le code de l’article

]]>
http://sametmax.com/refactoriser-ses-verifications-en-python/feed/ 5 6437
Django-quicky: l’abolition des préliminaires, par Sam et Max http://sametmax.com/django-quicky-labolition-des-preliminaires-par-sam-et-max/ http://sametmax.com/django-quicky-labolition-des-preliminaires-par-sam-et-max/#comments Tue, 09 Oct 2012 15:22:30 +0000 http://sametmax.com/?p=2537 Bottle pour sa simplicité. J'aime Django pour sa puissance. Nous aimons tous les deux les jeux de mots graveleux à conotations sexuelles. Ainsi est né django-quicky, une petite app qui permet de faire du routing et du rendering déclaratif en Django.]]> Max aime Bottle pour sa simplicité. J’aime Django pour sa puissance. Nous aimons tous les deux les jeux de mots graveleux à connotations sexuelles.

Ainsi est né django-quicky, une petite app qui permet de faire du routing et du rendering déclaratif en Django.

pip install django-quicky

Par toutes les routes

Vous avez envie d’un site, maintenant, tout de suite. Mais il faut créer l’urls.py, et les vues, et les mapper. Et jongler entre les deux fichiers.

Ou alors vous pouvez juste faire:

from django_quicky import routing

url, urlpatterns = routing()


@url('/une/regex/que/django/comprends')
def une_vue(request):
    ...


@url('/on/peut/cummuler/les/routes')
@url('/une/regex/que/django/comprends')
def une_vue(request):
    ...

Le résultat est parfaitement compatible (et mélangeable) avec le routing habituel de Django. Il suffit juste d’utiliser views.py à la place d’urls.py, par exemple dans URL_ROOT ou dans include().

Rien ne vaut une bonne renderette

Declarer sa vue, c’est comme les jupes, c’est de plus en plus court avec le temps qui passe (et la méthode render()). Mais on peut faire encore plus court, genre limite tanga:

from django_quicky import view

@view(render_to='template.html')
def une_vue(request):
    return {'truc': truc}


@view(render_to='json')
def une_vue_json(request):
    return {'truc': truc}

Dans la première vue, le dico est utilisé comme contexte pour faire le rendu du template. Dans la seconde vue, le dico est retourné sérialisé en JSON.

On change de position ?

Des fois vous êtes sur une bonne lancée, mais vous voudriez changer juste un truc. Et garder le code précédent bien sûr. C’est d’ailleurs pour ça qu’on nous vend les CBV, c’est tellement plus flexible !

Vous aimez la souplesse ? Voici de quoi mettre les chevilles très près des oreilles:

from django_quicky import view

@view(render_to='template.html')
def vue_commune(request):
    return {'stuff': stuff}

@vue_commune.post()
def vue_POST(request, context):
    # do more stuff
    return context

@vue_commune.ajax(render_to='json')
def vue_AJAX(request, context):
    return context

On a ici une seule et même vue. La première partie est rendue sous forme de template si on y arrive par une requête GET ordinaire. La seconde si c’est une requête POST. La troisième si c’est une requête en AJAX, avec rendering JSON en prime. Et les deux dernières récupèrent toujours le résultat de la première avant de s’exécuter, ce qui permet d’avoir le code en commun dans la première.

C’est sous licence zlib.

]]>
http://sametmax.com/django-quicky-labolition-des-preliminaires-par-sam-et-max/feed/ 22 2537
Comprendre les décorateurs Python pas à pas (partie 2) http://sametmax.com/comprendre-les-decorateur-python-pas-a-pas-partie-2/ http://sametmax.com/comprendre-les-decorateur-python-pas-a-pas-partie-2/#comments Wed, 23 May 2012 03:30:05 +0000 http://sametmax.com/?p=683 Cet article a été mis à jour et contient maintenant du code Python en version 3

Dans la partie 1, nous avons vu comment fonctionnaient les décorateurs. Mais dans leur usage quotidien vous allez rencontrer des cas particuliers:

  • Comment faire si la fonction décorée attend des arguments ?
  • Comment changer le comportement d’un décorateur en lui passant des paramètres ?
  • Comment préserver l’introspection ?

Introspection

Un des grands avantages de Python, c’est qu’il permet une très forte introspection, c’est à dire qu’on peut accéder à énormément d’informations sur le code lui-même.

Par exemple, si vous mettez une docstring à une fonction:

def ma_fonction():
    """
        C'est une super fonction
    """
    pass

Vous pouvez ensuite récupérer la docstring très facilement:

>>> ma_fonction.__doc__ "\n            C'est une super fonction\n        "

Et vous pouvez la lire dans l’aide:

>>> help(ma_fonction)
Help on function ma_fonction in module __main__:

ma_fonction()
    C'est une super fonction
(END)

L’autocompletion, la liste des attributs, le nom de la classe, etc. Toutes ces choses sont rendues accessibles grâce à l’introspection.

Mais quand vous décorez une fonction, vous l’enrobez dans une autre, détruisant ces informations:

def decorateur_inutile(func):
    def wrapper():
        func()
    return wrapper

@decorateur_inutile
def ma_fonction():
    """
        C'est une super fonction
    """
    pass

>>> print(ma_fonction.__doc__)
None
>>> help(ma_fonction)
Help on function wrapper in module __main__:

wrapper()

En effet, ma_fonction contient maitenant wrapper et non la fonction initiale. Heureusement le module functool possède des outils pour y pallier.

Le plus utile est le décorateur @wraps, qui copie littéralement toutes les infos d’une fonction sur son wrapper:

from functools import wraps

def decorateur_inutile(func):
    @wraps(func) # il suffit de décorer le wrapper
    def wrapper():
        func()
    return wrapper

@decorateur_inutile
def ma_fonction():
    """
        C'est une super fonction
    """
    pass

Et tout s’arrange:

>>> ma_fonction.__doc__
"\n        C'est une super fonction\n    "

Fonction avec arguments

Jusqu’ici les fonctions que nous avons décorées n’attendaient pas d’arguments. Il faut en effet faire un petit effort supplémentaire pour les supporter.

# Pas de magie noire, c'est le wrapper qui passe l'argument:

def un_decorateur_passant_un_argument(fonction_a_decorer):

    def un_wrapper_acceptant_des_arguments(arg1, arg2):
        print("J'ai des arguments regarde :", arg1, arg2)
        fonction_a_decorer(arg1, arg2)

    return un_wrapper_acceptant_des_arguments

# Puisqu'on appelle en fait un_wrapper_acceptant_des_arguments(),
# il accepte les arguments, et les passe à la fonctions décorée

@un_decorateur_passant_un_argument
def afficher_nom(nom, prenom):
    print("Mon nom est", nom, prenom)

afficher_nom("Peter", "Venkman")
# output:
#J'ai des arguments regarde : Peter Venkman
#Mon nom est Peter Venkman

Du coup pour décorer une méthode, il suffit d’accepter que le décorateur accepte self. Le moyen le plus simple est encore d’accepter *args, **kwargs, comme ça on est paré pour tous les cas.

Mais attention, si vous acceptez *args, **kwargs, la liste des arguments ne sera plus disponible pour l’introspection. C’est quelque chose que @wraps ne peut pas changer. La plupart du temps, c’est un compromis acceptable.

Passer un argument au décorateur lui-même

Le problème d’un décorateur, c’est qu’il doit accepter une fonction en paramètre. Pourtant, vous avez bien vu que @wraps accepte lui même un argument. C’est qu’il existe donc un moyen de passer un argument au décorateur lui-même.

La solution est tordue: créer un décorateur à la volée. En fait ce decorateur ne sera plus le décorateur, mais le créateur de décorateur. Il y aura donc 3 niveaux d’imbrication… C’est parti pour une session de vaudou :

def createur_de_decorateur():

    print("Je fabrique des décorateurs. Je suis éxécuté une seule fois :" +
           "à la création du décorateur")

    def mon_decorateur(func):

        print("Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction")

        def wrapper():
            print("Je suis le wrapper autour de la fonction décorée. "
                  "Je suis appelé quand on appelle la fonction décorée. "
                  "En tant que wrapper, je retourne le RESULTAT de la fonction décorée.")
            return func()

        print("En tant que décorateur, je retourne le wrapper")

        return wrapper

    print("En tant que créateur de décorateur, je retourne un décorateur")
    return mon_decorateur

# Créons un décorateur, c'est juste une fonction après tout.
nouveau_decorateur = createur_de_decorateur()
#ouputs:
#Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur.
#En tant que créateur de décorateur, je retourne un décorateur

# Ensuite décorons la fonction

def fonction_decoree():
    print("Je suis la fonction décorée")

fonction_decoree = nouveau_decorateur(fonction_decoree)
#ouputs:
#Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction
#En tant que décorateur, je retourne la fonction décorée

# Appelons la fonction:
fonction_decoree()
#ouputs:
#Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée.
#En tant que wrapper, je retourne le RESULTAT de la fonction décorée.
#Je suis la fonction décorée

Aucune surprise ici. Faisons EXACTEMENT la même chose, mais en sautant les variables intermédiares.

def fonction_decoree():
    print("Je suis la fonction décorée")
fonction_decoree = createur_de_decorateur()(fonction_decoree)
#ouputs:
#Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur.
#En tant que créateur de décorateur, je retourne un décorateur
#Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction
#En tant que décorateur, je retourne la fonction décorée.

# Au final:
fonction_decoree()
#ouputs:
#Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée.
#En tant que wrapper, je retourne le RESULTAT de la fonction décorée.
#Je suis la fonction décorée

On recommence, en encore plus court::

@createur_de_decorateur()
def fonction_decoree():
    print("Je suis la fonction décorée")
#ouputs:
#Je fabrique des décorateurs. Je suis éxécuté une seule fois : à la création du décorateur.
#En tant que créateur de décorateur, je retourne un décorateur
#Je suis un décorateur, je suis éxécuté une seule fois quand on décore la fonction
#En tant que décorateur, je retourne la fonction décorée.

#Et pour finir:
fonction_decoree()
#ouputs:
#Je suis le wrapper autour de la fonction décorée. Je suis appelé quand on appelle la fonction décorée.
#En tant que wrapper, je retourne le RESULTAT de la fonction décorée.
#Je suis la fonction décorée

Vous noterez qu’on a utilisé la notation @, avec un appel de fonction: @createur_de_decorateur() et non @createur_de_decorateur !

Maintenant que nous pouvons générer des décorateurs à la volée, il suffit de passer des arguments au créateur de décorateur:

def createur_de_decorateur_avec_arguments(decorator_arg1, decorator_arg2):

    print("Je créé des décorateur et j'accepte des arguments:", decorator_arg1, decorator_arg2)

    def mon_decorateur(func):
        print("Je suis un décorateur, vous me passez des arguments:", decorator_arg1, decorator_arg2)

        # Ne pas mélanger les arguments du décorateurs et de la fonction !
        def wrapped(function_arg1, function_arg2) :
            print("Je suis le wrapper autour de la fonction décorée.\n"
                  "Je peux accéder à toutes les variables\n"
                  "\t- du décorateur: {0} {1}\n"
                  "\t- de l'appel de la fonction: {2} {3}\n"
                  "Et je les passe ensuite à la fonction décorée"
                  .format(decorator_arg1, decorator_arg2,
                          function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return mon_decorateur

@createur_de_decorateur_avec_arguments("Leonard", "Sheldon")
def fonction_decoree_avec_arguments(function_arg1, function_arg2):
    print("Je suis une fonctions décorée, je ne me soucie que de mes arguments: {0}"
           " {1}".format(function_arg1, function_arg2))

fonction_decoree_avec_arguments("Rajesh", "Howard")
#output:
#Je crée des décorateurs et j'accepte des arguments: Leonard Sheldon
#Je suis un décorateur, vous me passez des arguments: Leonard Sheldon
#Je suis le wrapper autour de la fonction décorée function.
#Je peux accéder à toutes les variables
#   - du décorateur: Leonard Sheldon
#   - de l'appel de la fonction: Rajesh Howard
#Et je les passe ensuite à la fonction décorée
#Je suis une fonction décorée, je ne me soucie que de mes arguments: Rajesh Howard

mon_decorateur a accès aux variables du scope supérieur car elles sont dans une closure. Vous ne pourrez donc pas les modifier.

Et voilà, un décorateur avec des arguments ! Les arguments peuvent être des
variables:

c1 = "Penny"
c2 = "Leslie"

@createur_de_decorateur_avec_arguments("Leonard", c1)
def fonction_decoree_avec_arguments(function_arg1, function_arg2):
    print("Je suis une fonctions décorée, je ne me soucie que de mes arguments:"
           " {0} {1}".format(function_arg1, function_arg2))

fonction_decoree_avec_arguments(c2, "Howard")
#output:
#Je créé des décorateurs et j'accepte des arguments: Leonard Penny
#Je suis un décorateur, vous me passez des arguments: Leonard Penny
#Je suis le wrapper autour de la fonction décorée function.
#Je peux accéder à toutes les variables
#   - du décorateur: Leonard Penny
#   - de l'appel de la fonction: Leslie Howard
#Et je les passe ensuite à la fonction décorée
#Je suis une fonctions décorée, je ne me soucie que de mes arguments: Leslie Howard

Comme vous le voyez, on peut passer des arguments au décorateur comme à n’importe quelle fonction en utilisant cette astuce. En fait on peut même utiliser *args, **kwargs. Mais rappelez-vous: les décorateurs sont appelés uniquement une fois, au moment de l’import du script. On ne peut pas changer leurs arguments a posteriori. Quand vous faites from x import ma_fonction, ma_fonction est déjà décorée, et on ne peut rien y changer.

Super, mais ça sert à quoi un décorateur ?

Ca a l’air chouette et tout, mais un exemple d’usage concret, ça aiderait quand même….

Et bien il y a 1000 possibilités. Parmi les usages classiques:

  • étendre la fonction d’une lib externe qu’on ne peut pas modifier;
  • gérer les permissions d’une fonction;
  • réagir aux arguments passés;
  • débugger.

Le principe est la réutilisabilité: on fait un seul code, et on décore plein de fonctions avec.

Exemple:

def benchmark(func):
    """
    Un décorateur qui affiche le temps qu'une fonction met à s'éxécuter
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print(func.__name__, time.clock()-t)
        return res
    return wrapper

def logging(func):
    """
    Un décorateur qui log l'activité d'un script.
    (Ok, en vrai ça fait un print, mais ça pourrait logger !)
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print(func.__name__, args, kwargs)
        return res
    return wrapper

def counter(func):
    """
    Compte et affiche le nombre de fois qu'une fonction a été exécutée
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print("{0} a été utilisée: {1}x".format(func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return string[::-1]

print(reverse_string("Karine alla en Irak"))
print(reverse_string("Sa nana snob porte de trop bons ananas"))

## reverse_string ('Karine alla en Irak',) {}
## wrapper 0.000132
## wrapper a été utilisée: 1x
## karI ne alla eniraK
## reverse_string ('Sa nana snob porte de trop bons ananas',) {}
## wrapper 0.000128
## wrapper a été utilisée: 2x
## sanana snob port ed etrop bons anan aS

Mais bien sur, le plus cool avec les décorateurs, c’est qu’on peut les utiliser immédiatement sans avoir à réécrire quoi ce que soit:

import urllib

@counter
@benchmark
@logging
def citation_de_futurama_au_hasard():
    url = 'http://subfusion.net/cgi-bin/quote.pl?quote=futurama&number=1'
    try:
        res = urllib.request.urlopen(url)
        html = res.read().decode('ISO-8859-1')
        return html.split('
')[3].strip() except: return "No, I'm ... doesn't!" print(citation_de_futurama_au_hasard()) print(citation_de_futurama_au_hasard()) #output: #citation_de_futurama_au_hasard () {} #wrapper 0.02 #wrapper a été utilisée: 1x #The laws of science be a harsh mistress. #citation_de_futurama_au_hasard () {} #wrapper 0.01 #wrapper a été utilisée: 2x #Curse you, merciful Poseidon!

Python vient chargé de décorateurs dans la lib standard: property, staticmethod, classmethod, @coroutine, @lru_cache, etc. Django gère les permissions des vues avec les décorateurs. Bottle déclare ses routes avec. Twisted donne l’impression qu’un appel asynchrone est synchrone en les utilisant. On peut faire vraiment tout et n’importe quoi.

Un grand merci à gawel, de l’AFPY, qui m’a, il y a quelques années, donné envie de découvrir les décorateurs.

]]>
http://sametmax.com/comprendre-les-decorateur-python-pas-a-pas-partie-2/feed/ 30 683
Comprendre les décorateurs Python pas à pas (partie 1) http://sametmax.com/comprendre-les-decorateurs-python-pas-a-pas-partie-1/ http://sametmax.com/comprendre-les-decorateurs-python-pas-a-pas-partie-1/#comments Mon, 30 Apr 2012 22:32:26 +0000 http://sametmax.com/?p=480 Cet article a été mis à jour et contient maintenant du code Python en version 3

Les fonctions Python sont des objets

Pour comprendre les décorateurs, il faut d’abord comprendre que les fonctions sont des objets en Python. Cela a d’importantes conséquences:

def crier(mot="yes"):
    return mot.capitalize() + "!"

print(crier())
# output : 'Yes!'

# Puisque les fonctions sont des objets,
# on peut les assigner à des variables

hurler = crier

# Notez que l'on n’utilise pas les parenthèses :
# la fonction n'est pas appelée. Ici nous mettons la fonction "crier"
# dans la variable "hurler" afin de pouvoir appeler "crier" avec "hurler"

print(hurler())
# output : 'Yes!'

# Et vous pouvez même supprimer l'ancien nom "crier",
# la fonction restera accessible avec "hurler"

del crier
try:
    print(crier())
except NameError as e:
    print(e)
    #output: "name 'crier' is not defined"

print(hurler())
# output: 'Yes!'

Gardez ça à l’esprit, on va y revenir.

Une autre propriété intéressante des fonctions en Python est qu’on peut les définir à l’intérieur… d’une autre fonction.

def parler():

    # On peut définir une fonction à la volée dans "parler" ...
    def chuchoter(mot="yes"):
        return mot.lower()+"...";

    # ... et l'utiliser immédiatement !

    print(chuchoter())

# On appelle "parler", qui définit "chuchoter" A CHAQUE APPEL,
# puis "chuchoter" est appelé à l’intérieur de "parler"

parler()
# output:
# "yes..."

# Mais "chuchoter" N'EXISTE PAS en dehors de "parler"

try:
    print(chuchoter())
except NameError, e:
    print(e)
    #output : "name 'chuchoter' is not defined"

Passage des fonctions par référence

Toujours là ? Maintenant la partie amusante: vous avez vu que les fonctions sont des objets et peuvent donc:

  • être assignées à une variable;
  • être définies dans une autre fonction.

Cela veut dire aussi qu’une fonction peut retourner une autre fonction :-) Hop:

def creerParler(type="crier"):

    # On fabrique 2 fonctions à la volée
    def crier(mot="yes"):
        return mot.capitalize() + "!"

    def chuchoter(mot="yes") :
        return mot.lower() + "...";

    # Puis on retourne l'une ou l'autre
    if type == "crier":
        # on utilise pas "()", on n’appelle pas la fonction
        # on retourne l'objet fonction
        return crier
    else:
        return chuchoter

# Comment ce truc bizarre s'utilise ?

# Obtenir la fonction et l'assigner à une variable
parler = creerParler()

# "parler" est une variable qui contient la fonction "crier":
print(parler)
#output : 

# On peut appeler "crier" depuis "parler":
print(parler())
#ouput : YES!

# Et si on se sent chaud, on peut même créer et appeler la
# fonction en une seule fois:
print(creerParler("chuchoter")())
#output : yes...

Mais c’est pas fini ! Si on peut retourner une fonction, on peut aussi en passer une en argument…

def faireQuelqueChoseAvant(fonction):
    print("Je fais quelque chose avant d'appeler la fonction")
    print(fonction())

faireQuelqueChoseAvant(hurler)
#output:
#Je fais quelque chose avant d'appeler la fonction
#Yes!

C’est bon, vous avez toutes les cartes en main pour comprendre les décorateurs. En effet, les décorateurs sont des wrappers, c’est à dire qu’ils permettent d’exécuter du code avant et après la fonction qu’ils décorent, sans modifier la fonction elle-même.

Décorateur artisanal

Comment on en coderait un à la main:

# Un décorateur est une fonction qui attend une autre fonction en paramètre
def decorateur_tout_neuf(fonction_a_decorer):

    # En interne, le décorateur définit une fonction à la volée: le wrapper.
    # Le wrapper va enrober la fonction originale de telle sorte qu'il
    # puisse exécuter du code avant et après celle-ci
    def wrapper_autour_de_la_fonction_originale():

        # Mettre ici le code que l'on souhaite exécuter AVANT que la
        # fonction s’exécute
        print("Avant que la fonction ne s’exécute")

        # Apperler la fonction (en utilisant donc les parenthèses)
        fonction_a_decorer()

        # Mettre ici le code que l'on souhaite exécuter APRES que la
        # fonction s’exécute
        print("Après que la fonction se soit exécutée")

    # Arrivé ici, la "fonction_a_decorer" n'a JAMAIS ETE EXECUTEE
    # On retourne le wrapper que l'on vient de créer.
    # Le wrapper contient la fonction originale et le code à exécuter
    # avant et après, prêt à être utilisé.
    return wrapper_autour_de_la_fonction_originale

# Maintenant imaginez une fonction que l'on ne souhaite pas modifier.
def une_fonction_intouchable():
    print("Je suis une fonction intouchable, on ne me modifie pas !")

une_fonction_intouchable()
#output: Je suis une fonction intouchable, on ne me modifie pas !

# On peut malgré tout étendre son comportement
# Il suffit de la passer au décorateur, qui va alors l'enrober dans
# le code que l'on souhaite, pour ensuite retourner une nouvelle fonction

une_fonction_intouchable_decoree = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable_decoree()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Puisqu’on y est, autant faire en sorte qu’à chaque fois qu’on appelle une_fonction_intouchable, c’est une_fonction_intouchable_decoree qui est appelée à la place. C’est facile, il suffit d’écraser la fonction originale par celle retournée par le décorateur :

une_fonction_intouchable = decorateur_tout_neuf(une_fonction_intouchable)
une_fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Je suis une fonction intouchable, on ne me modifie pas !
#Après que la fonction se soit exécutée

Et c’est exactement ce que les décorateurs font.

Les décorateurs, démystifiés

L’exemple précédent, en utilisant la syntaxe précédente :

@decorateur_tout_neuf
def fonction_intouchable():
    print("Me touche pas !")

fonction_intouchable()
#output:
#Avant que la fonction ne s’exécute
#Me touche pas !
#Après que la fonction se soit exécutée

C’est tout. Oui, c’est aussi bête que ça.

@decorateur_tout_neuf est juste un raccourci pour

fonction_intouchable = decorateur_tout_neuf(fonction_intouchable)

Les décorateurs sont juste une variante pythonique du classique motif de conception “décorateur”.

Et bien sûr, on peut cumuler les décorateurs:

def pain(func):
    def wrapper():
        print("")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomates#")
        func()
        print("~salade~")
    return wrapper

def sandwich(food="--jambon--"):
    print(food)

sandwich()
#output: --jambon--
sandwich = pain(ingredients(sandwich))
sandwich()
#output:
#
# #tomates#
# --jambon--
# ~salade~
#<\______/>

Avec la syntaxe Python :

@pain
@ingredients
def sandwich(nourriture="--jambon--"):
    print(nourriture)

sandwich()
#output:
#
# #tomates#
# --jambon--
# ~salade~
#<\______/>

Avec cet exemple, on voit aussi que l’ordre d’application des décorateurs a de l’importance :

@ingredients
@pain
def sandwich_zarb(nourriture="--jambon--"):
    print(nourriture)

sandwich_zarb()
#output:
##tomates#
#
# --jambon--
#<\______/>
# ~salade~

Vous pouvez maintenant éteindre votre ordinateur et reprendre une activité normale.

Aller à la partie 2.

]]>
http://sametmax.com/comprendre-les-decorateurs-python-pas-a-pas-partie-1/feed/ 19 480