Le décorateur @property en Python

A quoi sert ce décorateur en python ?

Le problème exposé dans ce sujet a été résolu.

Salut les clem's,

J'ai découvert les décorateurs en Python il y a pas longtemps et je les trouve fascinant mais par hasard je suis tombée sur ce décorateur @property, j'ai fait mes petites recherches, j'ai juste compris qu'il faisait références aux mutateurs et acesseurs de python.

Donc ma question serait : Comment on s'en sert ? Et pourquoi ?

Merci d'avance :)

Ben à titre d'exemple j'en utilise à deux endroits sur le projet sur lequel je bosse en ce moment (un compilateur).

Autre cas beaucoup plus simple d'utilisation. Dans mon lexeur, j'ai :

  • une liste tokens qui contient les noms de mes classes de lexèmes.
  • pour chaque token, une fonction sous la forme :
1
2
3
4
def t_NOM_DU_TOKEN(t):
    r'expression régulière associée'
    # code de la fonction
    return t  

Pour être sûr de ne jamais oublier de modifier la liste chaque fois que j'ajoute une telle fonction, j'ai écrit un petit décorateur :

1
2
3
4
def addtoken(fn):
    '''Decorator to define a token through the corresponding t_* function'''
    tokens.append(fn.__name__.replace('t_', ''))
    return fn

Ce qui me permet de déclarer mes tokens et mes fonctions dans la même foulée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@addtoken
def t_INT_TYPE(t):
    r'(u|s)8|(u|s)(16|32|64)(h|n)?'
    return t

@addtoken
def t_STR_CONST(t):
    r'".*?[^\\]"'
    t.value = t.value[1:-1]
    return t

Ce code va automatiquement rajouter 'INT_TYPE' et 'STR_CONST' dans ma liste tokens.

En gros, les décorateurs permettent de gagner du temps, mais il faut un peu d'imagination et pas mal de recul sur ton code pour trouver des cas d'utilisation pertinents.

+2 -0

Un autre exemple, quand je veux avoir des infos sur des fonctions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def getInfos(f):
    def wrap(*args):
        args = tuple(map(str, args))
        res = f(*args)
        print("fonction utilisée : {}".format(f.__name__))
        print("arguments : {}".format(' '.join(args)))
        print("type de retour : {}".format(type(f(*args))))
        print("result : {}".format(res))
    return wrap

@getInfos
def test_1(a, b, c):
    return a, b, c

@getInfos
def test_2(a, b, c):
    return a

test_1(1, 2, 3)

test_2("a", "b", "c")

Je l'utilise aussi pour mesurer les temps d'exécution de chaque fonction, etc…

Un autre exemple pertinent je trouve est l'utilisation qui en est faite avec des frameworks tels que Bottle. Ils permettent en effet de définir aisément le routage des URLs en associant un motif à une vue.

1
2
3
4
5
6
7
@route('/hello/<name>')
def profile(name):
    return template('<b>Hello {{name}}</b>!', name=name)

@route('/')
def home():
    return 'Homepage'

@fred: attention, ton wrapper ne retourne pas le résultat de la fonction décorée.

Oui car j'utilise l'affichage du résultat avec print… Je ne l'avais pas mis pour éviter la redite du résultat ;)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def getInfos(f):
    def wrap(*args):
        args = tuple(map(str, args))
        res = f(*args)
        print("fonction utilisée : {}".format(f.__name__))
        print("arguments : {}".format(' '.join(args)))
        print("type de retour : {}".format(type(res)))
        print("result : {}".format(res))
        return res
    return wrap

@getInfos
def test_1(a, b, c):
    return a, b, c

@getInfos
def test_2(a, b, c):
    return a

print(test_1(1, 2, 3))

print(test_2("a", "b", "c"))
+0 -0

D'ailleurs pendant que j'y pense, vous vous servez souvent des metaclass ?

Pour essayer, mais j'en ai jamais eu l'intérêt dans un problème posé…

Comme le dirait un grand de python (Tim Peters)

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

Il me semble avoir déjà lu que les métaclasses sont souvent utilisées dans la construction d'API

+1 -0

Moi m'sieur, moi m'sieur ! J'ai déjà vu des métaclasses utilisées brillamment dans un projet auquel j'ai participé.

En gros, on avait un certain nombre d'objets très variés qui devaient être enregistrés automatiquement lors de l'exécution d'un programme. Problème : le programme en question était en développement, et les enregistrements ne devaient pas être brisés en cas de modification d'une classe d'un de ces objets. Pour ça, mon chef de projet a mis en place un système de versionnage assez complexe ; les classes instanciant des objets à enregistrer héritent d'une classe mère, appelons-la OBase (elle contient notamment les méthodes d'enregistrement, etc.). OBase a pour métaclasse MetaOBase, et possède un attribut de classe _version ; à l'exécution du programme, au moment de reconstruire chaque objet désérialisé, Python passe par cette métaclasse et si la version a changé, elle se charge de mettre à jour l'objet en conséquence, en récupérant la classe de versionnage nécessaire, et en faisant plein d'autres choses compliquées dont je ne me rappelle plus en détail.

Franchement, c'était magnifique, et en plus ça marchait ! :p

+1 -0

Haha ce mec était un fou. Il a porté ce projet à bout de bras, et même si je dois avoir à mon actif une dizaine de milliers de lignes de codes pour lui, j'ai l'impression de n'avoir rien fait. Il a construit from scratch un moteur titanesque, une vraie usine à gaz, pour que les gens qui bossaient avec lui n'aient à coder que les plus hautes couches d'abstraction… Avec le recul, je pense qu'il avait des journées de 36h, c'pas possible autrement. ^^

+0 -0

En fait, pour répondre de façon plus générale à la question des métaclasses, celles-ci deviennent utiles à partir du moment où tu veux modifier une classe juste avant qu'elle ne soit définie.

Niveau fonctionnalité, ça peut ressembler à l'application d'un décorateur sur une classe. En fait, c'est sensiblement différent parce qu'un décorateur intervient juste après que l'objet qu'il décore ne soit défini. De plus, la métaclasse est héritée par les classes filles, à la différence d'un décorateur de classe qui ne s'applique qu'à son argument.

Prenons un exemple qui va nous permettre de voir comment se débrouille Python.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyMetaClass(type):
    def __new__(cls, clsname, bases, dct):
        print("MyMetaClass", clsname, dct)
        # Ici on peut modifier le nom que portera la classe
        # Ou bien lui rajouter ou lui supprimer des classes mères
        # Ou bien modifier ses attributs de classe ou ses méthodes
        return super().__new__(cls, clsname, bases, dct)

def decorator(obj):
    print(obj, "was just defined")
    return obj

@decorator
class MyClass(metaclass=MyMetaClass):
    @decorator
    def __new__(cls, *args):
        print("MyClass.__new__", *args)
        return super().__new__(cls)

    @decorator
    def __init__(self, bar):
        print("MyClass.__init__", bar)
        self.bar = bar

a = MyClass("foo")

À l'exécution, ça donne :

1
2
3
4
5
6
<function MyClass.__new__ at 0x7f35df171bf8> was just defined
<function MyClass.__init__ at 0x7f35df171c80> was just defined
MyMetaClass MyClass {'__qualname__': 'MyClass', '__module__': '__main__', '__init__': <function MyClass.__init__ at 0x7f35df171c80>, '__new__': <function MyClass.__new__ at 0x7f35df171bf8>}
<class '__main__.MyClass'> was just defined
MyClass.__new__ foo
MyClass.__init__ foo

Dans l'ordre :

  • Python a compilé et défini la méthode __new__ de la classe MyClass. Cette méthode est la toute première que Python appelle pour instancier un objet.
  • Python a compilé et défini la méthode __init__ de la classe MyClass.
  • C'est là que c'est intéressant. Il n'a même pas encore fini de définir la classe MyClass qu'il instancie MyMetaClass avec trois arguments :
    • le nom de la classe MyClass dont MyMetaClass est la méta-classe.
    • le tuple des classes dont hérite MyClass (en dehors de object). Ici il n'y en a aucune.
    • le dictionnaire des attributs de MyClass

Cet appel à __new__ est chargé de construire la classe. En gros, puisque tout est objet en Python, ça revient à instancier la classe type. Cela veut dire que dans cette méthode, on peut modifier la classe elle-même avant qu'elle ne soit créée (ou même après, du moment que l'on retourne la classe à la fin). Par exemple on peut lui rajouter des méthodes ou des attributs dynamiquement.

  • Une fois que MyMetaClass.__new__() retourne, l'objet résultant (qui est une classe), est enregistrée dans le scope courant sous le nom MyClass.

On en arrive à la dernière ligne du script : qui illustre simplement le fait que __new__ soit le constructeur à proprement parler de la classe, puisqu'il est appelé avant __init__.

+0 -0

Haha ce mec était un fou. Il a porté ce projet à bout de bras, et même si je dois avoir à mon actif une dizaine de milliers de lignes de codes pour lui, j'ai l'impression de n'avoir rien fait. Il a construit from scratch un moteur titanesque, une vraie usine à gaz, pour que les gens qui bossaient avec lui n'aient à coder que les plus hautes couches d'abstraction… Avec le recul, je pense qu'il avait des journées de 36h, c'pas possible autrement. ^^

Alkareth

Ce mec est-t-il un dieu en python ? Je pense que oui.

Nohar : Ta réponse est très complète et très fournie, je garde cette page quand j'appendrai les meta-class en python, je l'encadrerai sur le mur de ma chambre ;)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte