Utilisation de __metaclass__ en python pour définir des champs d’objet typés

Si je veux assigner un un champ d’un objet en python, il me suffit de faire : monObject.champ = valeur. Python est un langage non statiquement typé, et les objets sont des dictionnaires auxquels je peux rajouter des entrées à volonté!
Ce n’est pas dans l’esprit de python de vérifier les types des données systématiquement, mais comment faire en cas de réelle contrainte sur les dits types?



On peut déjà utiliser des champs de type property qui permettent de définir des fonctions pour accéder aux éléments de l’objet.

# Une classe dont je souhaite que le champ
# field soit obligatoirement un int
class MaClasse(object):
    @property # le getter
    def field(self):
        return self._field

    @field.setter
    def field(self, value):
        if not isinstance(value, int):
            raise TypeError()
        else:
            self._field = value

Ça marche, mais on se rend compte que je dois coder une fonction de getter et une fonction de setter pour chaque champ que je veux contrôler… et ça peut rapidement être très contraignant.

Pour faire plus simple… Je me rappelle que les classes python sont elles-mêmes des objets servant de prototypes aux instances. Ce qui veut dire d’une part que je peux modifier une classe au runtime, et d’autre part qu’il existe un constructeur pour une classe. Pour ces deux opérations, j’utilise le champ __metaclass__ de ma classe qui va me permettre d’accéder à la méthode __new__, le constructeur de ma classe.

Je veux utiliser une classe DataField qui me servira de descripteur de champ. Voici à quoi je voudrais que ma classe finale ressemble.

class DataField(object):
    def __init__(self, type):
        self.type = type

from datetime import datetime
class MaClasse(object):
    id = DataField(int)
    label = DataField(str)
    date = DataField(datetime)

Il faudrait que chacun de ces DataFields devienne automatiquement une property qui ferait la vérification de type dans le code du setter.

C’est là que __metaclass__ entre en jeu en permettant de surcharger le code __new__ de la classe.

class MonMeta(type):
    def __new__(cls, name, parents_cls, attributes):
        # Appel du constructeur parent
        newcls = super(MonMeta, cls).__new__(cls,
                                            name,
                                            parents_cls,
                                            attributes)
        # On boucle sur les attributs
        for attrname, attr in attributes.items():
            # le cas qui nous intéresse:
            if isinstance(attr, DataField):
                # le nom de la variable qui stocke la valeur
                attr.attrname = attrname
                private_name = '_' + attrname

                # le getter (voir ci-après)
                getter = attr.build_getter(private_name)

                # le setter (voir ci-après)
                setter = attr.build_setter(private_name)

                # On génère la property
                prop = property(getter, setter)
                # On remplace le champ par la property
                setattr(newcls, attrname, prop)
        return newcls

Qu’est ce donc que ces méthodes build_getter et build_setter? Elles ne tombent pas du ciel et il faut les implémenter. On les rajoute à la classe DataField, de façon à obtenir ceci :

class DataField(object):
    def __init__(self, type):
        self.type = type

    # Construit et retourne la fonction qui servira de getter
    def build_getter(self, private_name):
        def getter(obj):
            return getattr(obj, private_name, None)
        return getter
    
    # Construit et retourne la fonction qui servira de setter
    def build_setter(self, private_name):
        def setter(obj, value):
            if not isinstance(value, self.type):  
                raise TypeError('%s.%s doit être de type %s'  
                            % (obj.__class__.__name__, self.attrname, self.type.__name__))  
            else:  
                setattr(obj, private_name, value)
        return setter

On instancie une fonction, pour chaque champ, qui décrit comment accéder à la véritable valeur en lecture et en écriture.

Ensuite, il ne reste plus qu’à indiquer quel metaclass utiliser dans MaClasse.

class MaClasse(object):
    __metaclass__ = MonMeta
    .
    .
    .

Donc, au chargement de ma classe au runtime, la fonction __new__ de sa metaclass sera appelée, et mes champs DataField seront remplacés par des champs de type property qui feront les bonnes vérifications. Je peux même avoir des classes héritières de MaClasse, la metaclass sera propagée.

>>> o = MaClasse()
>>> o.id = 5
>>> print o.id
5
>>> o.date = "une chaine"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: MaClasse.date doit être de type datetime

Pour voir plus loin, on peut penser à donner des valeurs par défaut, ou bien encore affiner la possibilité de modifier/effacer des champs… Adaptez le à vos besoins! Pour le détail, je vous laisse voir la documentation de __metaclass__.

Vous aimerez aussi...

2 réponses

  1. Pierre dit :

    Bonjour!
    J’ai trouvé cet article très intéressant et très didactique, cependant le code ne fonctionne pas pour moi. Aurais-je fait une erreur quelque part?
    Pourtant en copiant-collant le code j’ai cette erreur:
    Traceback (most recent call last):
    File « /home/pier/eclipse_workspace/testing stuff/src/metaclass.py », line 51, in
    a.id = 45
    File « /home/pier/eclipse_workspace/testing stuff/src/metaclass.py », line 32, in setter
    % (attrname, attr.type.__name__))
    TypeError: label doit etre de type str

    J’ai l’impression qu’il y a la même fonction de getter et setter pour pour chacun des attributs id, label et date…

    • olivier dit :

      Il y a effectivement une erreur dans mon code. Je l’avais dérivé d’un code que j’utilise, mais à trop vouloir le simplifier, j’ai commis une erreur.
      En fait, de déclarer les fonctions getter et setter dans la boucle est là où tout bascule, car à la fin de la boucle, les valeurs pour attr et attrname ne bougent plus, et c ce sont donc ces dernières valeurs qu’utilisent le getter et le setter.

      J’ai donc modifié l’article de façon à ce que l’object DataField génère lui-même les fonctions qui lui sont liées, comme ça plus de soucis 🙂
      Dans la metaclass, on appelle désormais ces méthode build_getter et build_setter avant de construire la property.

Laisser un commentaire