Que sont les métaclasses ? A quoi servent-elles ?
TLDR : Une métaclasse instancie et définit le comportement d'une classe tout comme une classe instancie et définit le comportement d'une instance.
Pseudocode :
>>> Class(...)
instance
Ce qui précède devrait vous sembler familier. Eh bien, où se trouve Class
venir ? C'est une instance d'une métaclasse (également en pseudocode) :
>>> Metaclass(...)
Class
Dans le code réel, nous pouvons passer la métaclasse par défaut, type
tout ce dont nous avons besoin pour instancier une classe et nous obtenons une classe :
>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>
Présenter les choses différemment
-
Une classe est à une instance ce qu'une métaclasse est à une classe.
Quand on instancie un objet, on obtient une instance :
>>> object() # instantiation of class
<object object at 0x7f9069b4e0b0> # instance
De même, lorsque nous définissons explicitement une classe avec la métaclasse par défaut, type
on l'instancie :
>>> type('Object', (object,), {}) # instantiation of metaclass
<class '__main__.Object'> # instance
-
En d'autres termes, une classe est une instance d'une métaclasse :
>>> isinstance(object, type)
True
-
D'une troisième façon, une métaclasse est la classe d'une classe.
>>> type(object) == type
True
>>> object.__class__
<class 'type'>
Lorsque vous écrivez une définition de classe et que Python l'exécute, il utilise une métaclasse pour instancier l'objet de classe (qui sera, à son tour, utilisé pour instancier des instances de cette classe).
Tout comme nous pouvons utiliser des définitions de classe pour modifier le comportement des instances d'objets personnalisés, nous pouvons utiliser une définition de classe de métaclasse pour modifier le comportement d'un objet de classe.
A quoi peuvent-ils servir ? De la docs :
Les utilisations potentielles des métaclasses sont illimitées. Parmi les idées qui ont été explorées figurent la journalisation, la vérification des interfaces, la délégation automatique, la création automatique de propriétés, les mandataires, les cadres et le verrouillage/synchronisation automatique des ressources.
Néanmoins, il est généralement recommandé aux utilisateurs d'éviter d'utiliser des métaclasses, sauf en cas de nécessité absolue.
Vous utilisez une métaclasse chaque fois que vous créez une classe :
Lorsque vous écrivez une définition de classe, par exemple, comme ceci,
class Foo(object):
'demo'
Vous instanciez un objet de classe.
>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)
C'est la même chose que d'appeler fonctionnellement type
avec les arguments appropriés et en assignant le résultat à une variable de ce nom :
name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)
Notez que certains éléments sont automatiquement ajoutés à la __dict__
c'est-à-dire l'espace de noms :
>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>,
'__module__': '__main__', '__weakref__': <attribute '__weakref__'
of 'Foo' objects>, '__doc__': 'demo'})
Le site métaclasse de l'objet que nous avons créé, dans les deux cas, est type
.
(Un aparté sur le contenu de la classe __dict__
: __module__
est là car les classes doivent savoir où elles sont définies, et __dict__
et __weakref__
sont là parce que nous ne définissons pas __slots__
- si nous définir __slots__
nous économiserons un peu d'espace dans les instances, car nous pouvons interdire l'utilisation de __dict__
et __weakref__
en les excluant. Par exemple :
>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})
... mais je m'égare.)
Nous pouvons étendre type
comme toute autre définition de classe :
Voici l'option par défaut __repr__
de classes :
>>> Foo
<class '__main__.Foo'>
L'une des choses les plus précieuses que l'on peut faire par défaut en écrivant un objet Python est de lui fournir un bon fichier __repr__
. Lorsque nous appelons help(repr)
nous apprenons qu'il y a un bon test pour une __repr__
qui exige également un test d'égalité - obj == eval(repr(obj))
. L'implémentation simple suivante de __repr__
et __eq__
pour les instances de notre classe de type nous fournit une démonstration qui peut améliorer la méthode par défaut __repr__
de classes :
class Type(type):
def __repr__(cls):
"""
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> eval(repr(Baz))
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
"""
metaname = type(cls).__name__
name = cls.__name__
parents = ', '.join(b.__name__ for b in cls.__bases__)
if parents:
parents += ','
namespace = ', '.join(': '.join(
(repr(k), repr(v) if not isinstance(v, type) else v.__name__))
for k, v in cls.__dict__.items())
return '{0}(\'{1}\', ({2}), {{{3}}})'.format(metaname, name, parents, namespace)
def __eq__(cls, other):
"""
>>> Baz == eval(repr(Baz))
True
"""
return (cls.__name__, cls.__bases__, cls.__dict__) == (
other.__name__, other.__bases__, other.__dict__)
Ainsi, maintenant, lorsque nous créons un objet avec cette métaclasse, la fonction __repr__
en écho sur la ligne de commande fournit une vue beaucoup moins laide que la valeur par défaut :
>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
Avec une belle __repr__
définie pour l'instance de la classe, nous avons une meilleure capacité à déboguer notre code. Cependant, une vérification beaucoup plus poussée avec eval(repr(Class))
est peu probable (car il serait plutôt impossible d'évaluer les fonctions à partir de leur valeur par défaut). __repr__
's).
Un usage attendu : __prepare__
un espace de nom
Si, par exemple, nous voulons savoir dans quel ordre les méthodes d'une classe sont créées, nous pouvons fournir un dict ordonné comme espace de nom de la classe. Nous ferions cela avec __prepare__
dont renvoie le dict de l'espace de nom pour la classe si elle est implémentée en Python 3 :
from collections import OrderedDict
class OrderedType(Type):
@classmethod
def __prepare__(metacls, name, bases, **kwargs):
return OrderedDict()
def __new__(cls, name, bases, namespace, **kwargs):
result = Type.__new__(cls, name, bases, dict(namespace))
result.members = tuple(namespace)
return result
Et l'usage :
class OrderedMethodsObject(object, metaclass=OrderedType):
def method1(self): pass
def method2(self): pass
def method3(self): pass
def method4(self): pass
Et maintenant nous avons un enregistrement de l'ordre dans lequel ces méthodes (et d'autres attributs de classe) ont été créées :
>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')
Remarque, cet exemple a été adapté du documentation - le nouveau Enum dans la bibliothèque standard fait ça.
Donc ce que nous avons fait, c'est instancier une métaclasse en créant une classe. Nous pouvons également traiter la métaclasse comme n'importe quelle autre classe. Elle possède un ordre de résolution des méthodes :
>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)
Et il a approximativement la bonne repr
(que nous ne pouvons plus évaluer à moins de trouver un moyen de représenter nos fonctions) :
>>> OrderedMethodsObject
OrderedType('OrderedMethodsObject', (object,), {'method1': <function OrderedMethodsObject.method1 at 0x0000000002DB01E0>, 'members': ('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4'), 'method3': <function OrderedMet
hodsObject.method3 at 0x0000000002DB02F0>, 'method2': <function OrderedMethodsObject.method2 at 0x0000000002DB0268>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'OrderedMethodsObject' objects>, '__doc__': None, '__d
ict__': <attribute '__dict__' of 'OrderedMethodsObject' objects>, 'method4': <function OrderedMethodsObject.method4 at 0x0000000002DB0378>})