Métaclasse sous Python
Par kerflyn le 19/06/2006 07:34
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
- Python Metaclasses: Who? Why? When?. (en)
- David Mertz and Michele Simionato. Metaclass programming in Python: Pushing object-oriented programming to the next level. 26 Feb 2003 (en)
- David Mertz and Michele Simionato. Metaclass programming in Python, Part 2: Understanding the arcana of inheritance and instance creation. 28 Aug 2003 (en)
- Guido van Rossum. Unifying types and classes in Python 2.2. (en)
- Metaclasses in Python 1.5 (A.k.a. The Killer Joke :-)





