Métaclasse sous Python

Document Actions

Par kerflyn le 19/06/2006 07:34

Les métaclasses permettent d'ajouter des fonctionnalités au langage. Ce document défini la notion de métaclasse et montre comment créer des métaclasses sous Python.

Catégories : autre
Version Python : 2.2

Introduction

Les métaclasses offrent un moyen d'ajouter des fonctionnalités au langage. Elle permettent par exemple d'ajouter un contrat (précondition, postcondition) à certaines méthodes, de tracer les appels de méthodes, de sérialiser des objets au format XML, etc. Python propose la possibilité de créer des métaclasses.

Il est possible d'utiliser le concept de métaclasse sous Python depuis la version 1.5 (voir l'article Metaclasses in Python 1.5 cité en bas). Cependant, dans cette version la création de métaclasses était un vrai casse-tête. Mais ce concept est véritablement devenu accessible depuis la version 2.2.

Généralités

En programmation objet, une classe est une sorte de moule qui permet de créer un sous-ensemble d'objets. La classe décrit comment se comportent ses objets, fournit leur interface et compose la structure de leur état. La classe permet de créer de nouveaux objets au moyen d'un mécanisme appelé instanciation. Ce mécanisme peut se décomposer en deux opérations :

  • allocation d'un espace mémoire pour le nouvel objet (opération alloc()),
  • initialisatiion du nouvel objet (opération init()).

Tout comme Smalltalk et Ruby, Python est un environnement de programmation réflexif, c'est-à-dire que Python est capable de s'analyser et de se modifier lui-même. Dans un tel environnement, les classes peuvent être vues comme des objets à part entière créés au moyen du mécanisme d'instanciation (alloc(); init()). Dans ce cas, toutes les classes peuvent être vues comme des intances créées à partir d'une même classe.

Une classe dont les instances sont des classes se nomme métaclasse (notez que la définition est récursive). Puisqu'une métaclasse est une classe, elle définit elle aussi le comportement et la structure de l'état de ses instances. En créant une nouvelle métaclasse, on va donc intervenir sur la manière avec laquelle les classes sont créées et donc intervenir sur une partie du langage lui-même. Grâce à un mécanisme comme celui-ci, le développeur va pouvoir ajouter de nouvelles fonctionnalités au langage, comme la possibilité de tracer les appels de méthodes, la possibilité de créer des singletons, la sérialisation d'objets au format XML, etc. Sous Python, le mécanisme de métaclasse est suffisamment puissant pour permettre la création de telles fonctionnalités.

Métaclasse sous Python

Les métaclasses Python possèdent quelques spécificités par rapport aux concepts théoriques. La métaclasse de base se nomme type (qui est retourné par monObjet.__class__.__class__). Pour créer une nouvelle métaclasse, il suffit de dériver la métaclasse de base.

Que va-t-on trouver dans une métaclasse ? Les métaclasses permettent de créer des objets particuliers capables d'instancier. On va donc y décrire des méthodes représentant les deux opérations alloc() et init() et qui, sous Python, sont respectivement dénommées __new__() et __init__(). Ainsi, la métaclasse permet de maîtriser le processus d'instanciation. Dans les métaclasses, on peut aussi ajouter des attributs et des méthodes qui vont s'ajouter à l'état et à l'interface des classes instanciées. Ce seront donc des attributs et des méthodes de classe.

Les opérations alloc() et init() sont représentées par les méthodes de type __new__() et __init__(). __new__() est une méthode de classe qui prend quatre paramètres :

  • metacls : la métaclasse classe elle-même (remplace le traditionnel self, par convention),
  • name : le nom de la nouvelle classe,
  • bases : la liste des classes parentes de la nouvelle classe,
  • dct : le dictionnaire de l'ensemble des attributs et méthodes décrit dans la nouvelle classe.

__new__() doit absolument retourner un objet. De manière générale, cette méthode permet de réaliser des opérations de bas niveau sur des classes : changer l'interface, modifier les classes de base, donner un autre nom de classe, etc.

La méthode __init__() est une méthode d'instance. Elle possède les mêmes paramètres que la méthode __new__(), à l'exception du premier paramètre qui représente la nouvelle classe et qui est nommé cls par convention. __init__() permet de réaliser des opérations de plus haut niveau sur les classes : vérification, enregistrement, etc. Certaines opérations de bas niveau n'y sont plus possibles, comme changer le nom ou les classes de bases.

Lors de la création d'une instance, c'est la méthode __call__() qui est appelée et qui s'occupe de l'appel des méthodes __new__() et __init__(). En effet, l'expression MaClasse() équivaut à MaClasse.__call__(). Donc, modifier __call__() revient aussi à modifier le processus d'instanciation.

La liaison entre une classe et sa métaclasse se fait au moyen de la variable __metaclass__. Si elle est assignée dans une classe, seule cette classe est concernée. Si elle est assignée dans un module, ce sont toutes les nouvelles classes du module qui sont concernées.

Exemple

L'exemple ci-dessous montre un exemple d'utilisation d'une métaclasse en Python. Il s'agit d'une métaclasse qui permet de tracer l'appel de méthode.

import types

class Tracer(type):
    def __new__(metacls, name, bases, dct):
        # définition d'une fonction wrapper pour les méthodes
        def _wrapper(name, method):
            # création d'une fonction permettant tracer une méthode
            def _trace(self, *args, **kwargs):
                print "(call %s)" % name
                return method(self, *args, **kwargs)
            # idiome: faire passer une fonction pour une autre
            _trace.__name__ = method.__name__
            _trace.__doc__  = method.__doc__
            _trace.__dict__.update(method.__dict__)
            return _trace

        # remplacement de toutes les méthodes par leur équivalent tracé
        newDct = {}
        for name, slot in dct.iteritems():
            if type(slot) is types.FunctionType:
                newDct[name] = _wrapper(name, slot)
            else:
                newDct[name] = slot
        # appel au constructeur de la super classe (type)
        return type.__new__(metacls, name, bases, newDct)

Le principe de la métaclasse Tracer consiste à transformer toutes les méthodes de ses instances, de telle sorte qu'elles affichent un message avant de continuer leur exécution. La fonction _wrapper() qui se trouve dans le constructeur de la métaclasse est un décorateur, c'est-à-dire qu'elle prend en entrée une fonction et renvoie en sortie cette fonction avec un comportement modifier. Il faut savoir que dans le paramètre dct du constructeur, ce qui doit représenter des méthodes est encore à l'état de fonction.

Voici un exemple montrant comment utiliser la métaclasse.

class A(object):
    # création de la liaison entre la classe et la métaclasse
    __metaclass__ = Tracer

    def __init__(self):
        self.a = 1

    def get(self): return self.a
(call __new__)

>>> a = A()
(call __init__)
>>> a.get()
(call get)
1

Conclusion

Avec Python, nous sommes bien dans un environnement réflexif. D'un côté, nous avons créé un système permettant d'afficher des méthodes appelées. De l'autre, nous avons réussi à modifier dynamiquement le code de méthodes. Notre cas d'affichage est un cas particulier d'introspection, c'est-à-dire de découverte de soi. Quant au cas de modification dynamique, c'est un cas particulier d'intercession (intervention sur soi).

Les métaclasses sont un moyen pour intervenir sur des aspects fonctionnels du langage. On peut grâce à elles étendre, voire modifier, le comportement du langage. Bien plus qu'un gadget pour développeur, les métaclasses permettent au besoin de l'aider et de faciliter le processus de développement d'une application.

Bibliographie

Aidez l'AfPy
Dernières news AFPY
Les 6 dernières news
Initiation à Python 3
11/02/2010 06:01
 
Python.org : Le site officiel du langage Python.
Zope.org : Le site web officiel de Zope.
Daily Python-URL : Actus de l'univers Python.
Tribute to Zyons : Zyons notre ami et membre fondateur de l'Afpy, nous quittait en 2005.