Les classes en tant qu'objets
Avant de comprendre les métaclasses, vous devez maîtriser les classes en Python. Et Python a une idée très particulière de ce que sont les classes, empruntée au langage Smalltalk.
Dans la plupart des langages, les classes ne sont que des morceaux de code qui décrivent comment produire un objet. C'est un peu vrai en Python aussi :
>>> class ObjectCreator(object):
... pass
...
>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>
Mais les classes sont plus que cela en Python. Les classes sont aussi des objets.
Oui, des objets.
Dès que vous utilisez le mot-clé class
Python l'exécute et crée un OBJET. L'instruction
>>> class ObjectCreator(object):
... pass
...
crée en mémoire un objet avec le nom "ObjectCreator".
Cet objet (la classe) est lui-même capable de créer des objets (les instances), et c'est pourquoi c'est une classe .
Mais quand même, c'est un objet, et donc :
- vous pouvez l'assigner à une variable
- vous pouvez le copier
- vous pouvez y ajouter des attributs
- vous pouvez le passer comme un paramètre de fonction
Par exemple :
>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
... print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>
Créer des classes de façon dynamique
Les classes étant des objets, vous pouvez les créer à la volée, comme n'importe quel objet.
Tout d'abord, vous pouvez créer une classe dans une fonction en utilisant class
:
>>> def choose_class(name):
... if name == 'foo':
... class Foo(object):
... pass
... return Foo # return the class, not an instance
... else:
... class Bar(object):
... pass
... return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>
Mais ce n'est pas si dynamique, car vous devez toujours écrire vous-même la classe entière.
Puisque les classes sont des objets, elles doivent être générées par quelque chose.
Lorsque vous utilisez le class
Python crée automatiquement cet objet. Mais comme avec la plupart des choses en Python, il vous donne un moyen de le faire manuellement.
Rappelez-vous la fonction type
? La bonne vieille fonction qui vous permet de savoir quel type d'un objet :
>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>
Bien, type
a une capacité complètement différente, il peut aussi créer des classes à la volée. type
peut prendre la description d'une classe comme paramètres, et retourner une classe.
(Je sais, c'est idiot que la même fonction puisse avoir deux utilisations complètement différentes en fonction des paramètres que vous lui passez. C'est un problème dû à la rétrocompatibilité compatibilité en Python)
type
fonctionne de cette façon :
type(name, bases, attrs)
Où :
-
name
: nom de la classe
-
bases
: tuple de la classe parent (pour l'héritage, peut être vide)
-
attrs
: dictionnaire contenant les noms et les valeurs des attributs
Par exemple :
>>> class MyShinyClass(object):
... pass
peuvent être créés manuellement de cette manière :
>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>
Vous remarquerez que nous utilisons "MyShinyClass" comme nom de la classe et comme variable pour contenir la référence de la classe. Ils peuvent être différents, mais il n'y a aucune raison de compliquer les choses.
type
accepte un dictionnaire pour définir les attributs de la classe. Ainsi :
>>> class Foo(object):
... bar = True
Peut être traduit en :
>>> Foo = type('Foo', (), {'bar':True})
Et utilisé comme une classe normale :
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True
Et bien sûr, vous pouvez en hériter, donc.. :
>>> class FooChild(Foo):
... pass
serait :
>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True
Éventuellement, vous voudrez ajouter des méthodes à votre classe. Il suffit de définir une fonction avec la signature appropriée et de l'assigner comme attribut.
>>> def echo_bar(self):
... print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True
Et vous pouvez ajouter encore plus de méthodes après avoir créé dynamiquement la classe, tout comme vous ajoutez des méthodes à un objet de classe normalement créé.
>>> def echo_bar_more(self):
... print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True
Vous voyez où nous voulons en venir : en Python, les classes sont des objets, et vous pouvez créer une classe à la volée, de façon dynamique.
C'est ce que fait Python lorsque vous utilisez le mot-clé class
et il le fait en utilisant une métaclasse.
Que sont les métaclasses (enfin)
Les métaclasses sont le "matériel" qui crée les classes.
Vous définissez des classes afin de créer des objets, n'est-ce pas ?
Mais nous avons appris que les classes Python sont des objets.
Ce sont les métaclasses qui créent ces objets. Elles sont les classes des classes, vous pouvez les imaginer de cette façon :
MyClass = MetaClass()
my_object = MyClass()
Vous avez vu que type
vous permet de faire quelque chose comme ça :
MyClass = type('MyClass', (), {})
C'est parce que la fonction type
est en fait une métaclasse. type
est la que Python utilise pour créer toutes les classes en coulisse.
Maintenant, vous vous demandez pourquoi il est écrit en minuscules, et non pas Type
?
Eh bien, je suppose que c'est une question de cohérence avec str
la classe qui crée les objets des objets chaînes de caractères, et int
la classe qui crée les objets entiers. type
est juste la classe qui crée les objets de classe.
Vous voyez qu'en vérifiant le __class__
attribut.
Tout, et je dis bien tout, est un objet en Python. Cela inclut les ints, les chaînes de caractères, les fonctions et les classes. Tous sont des objets. Et tous ces objets ont été créés à partir d'une classe :
>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>
Maintenant, quel est le __class__
de tout __class__
?
>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>
Donc, une métaclasse est juste le truc qui crée des objets de classe.
Vous pouvez l'appeler une "fabrique de classes" si vous le souhaitez.
type
est la métaclasse intégrée que Python utilise, mais bien sûr, vous pouvez créer votre propre métaclasse.
En Python 2, vous pouvez ajouter une fonction __metaclass__
lorsque vous écrivez une classe (voir la section suivante pour la syntaxe de Python 3) :
class Foo(object):
__metaclass__ = something...
[...]
Si vous le faites, Python utilisera la métaclasse pour créer la classe Foo
.
Attention, c'est délicat.
Vous écrivez class Foo(object)
d'abord, mais l'objet de classe Foo
n'est pas encore créé dans la mémoire.
Python cherchera __metaclass__
dans la définition de la classe. S'il la trouve, il l'utilisera pour créer la classe d'objets Foo
. Si ce n'est pas le cas, il utilisera type
pour créer la classe.
Lisez ça plusieurs fois.
Quand vous le faites :
class Foo(Bar):
pass
Python fait ce qui suit :
Y a-t-il un __metaclass__
l'attribut dans Foo
?
Si oui, créez en mémoire un objet de classe (j'ai dit un objet de classe, restez avec moi ici), avec le nom Foo
en utilisant ce qui est dans __metaclass__
.
Si Python ne peut pas trouver __metaclass__
il cherchera un __metaclass__
au niveau du MODULE, et essayer de faire la même chose (mais seulement pour les classes qui n'héritent de rien, en gros les classes à l'ancienne).
Ensuite, s'il ne trouve pas de __metaclass__
du tout, il utilisera le Bar
La métaclasse propre à l'utilisateur (le premier parent) (qui pourrait être la métaclasse par défaut type
) pour créer l'objet classe.
Faites attention ici que le __metaclass__
ne sera pas hérité, la métaclasse du parent ( Bar.__class__
) le sera. Si Bar
a utilisé un __metaclass__
qui a créé Bar
avec type()
(et non type.__new__()
), les sous-classes n'hériteront pas de ce comportement.
Maintenant la grande question est, que pouvez-vous mettre dans __metaclass__
?
La réponse est quelque chose qui peut créer une classe.
Et qu'est-ce qui peut créer une classe ? type
ou tout ce qui le sous-classe ou l'utilise.
Métaclasses en Python 3
La syntaxe pour définir la métaclasse a été modifiée dans Python 3 :
class Foo(object, metaclass=something):
...
c'est-à-dire le __metaclass__
n'est plus utilisé, au profit d'un argument mot-clé dans la liste des classes de base.
Le comportement des métaclasses reste cependant en grande partie les mêmes .
Une chose ajoutée aux métaclasses dans Python 3 est que vous pouvez également passer des attributs comme arguments de mot-clé dans une métaclasse, comme ceci :
class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):
...
Lisez la section ci-dessous pour savoir comment python gère cela.
Métaclasses personnalisées
Le but principal d'une métaclasse est de changer automatiquement la classe lorsqu'elle est créée.
Vous faites généralement cela pour les API, où vous voulez créer des classes correspondant à la contexte actuel.
Imaginez un exemple stupide, où vous décidez que toutes les classes de votre module doivent avoir leurs attributs écrits en majuscules. Il existe plusieurs façons de de le faire, mais l'une d'entre elles consiste à définir __metaclass__
au niveau du module.
De cette façon, toutes les classes de ce module seront créées en utilisant cette métaclasse, et nous devons juste dire à la métaclasse de mettre tous les attributs en majuscules.
Heureusement, __metaclass__
peut en fait être n'importe quel appelable, il n'a pas besoin d'être une classe formelle (je sais, quelque chose avec 'class' dans son nom n'a pas besoin d'être une classe, allez savoir... mais c'est utile).
Nous allons donc commencer par un exemple simple, en utilisant une fonction.
# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
"""
Return a class object, with the list of its attribute turned
into uppercase.
"""
# pick up any attribute that doesn't start with '__' and uppercase it
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in future_class_attrs.items()
}
# let `type` do the class creation
return type(future_class_name, future_class_parents, uppercase_attrs)
__metaclass__ = upper_attr # this will affect all classes in the module
class Foo(): # global __metaclass__ won't work with "object" though
# but we can define __metaclass__ here instead to affect only this class
# and this will work with "object" children
bar = 'bip'
Vérifions :
>>> hasattr(Foo, 'bar')
False
>>> hasattr(Foo, 'BAR')
True
>>> Foo.BAR
'bip'
Maintenant, faisons exactement la même chose, mais en utilisant une vraie classe pour une métaclasse :
# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
# __new__ is the method called before __init__
# it's the method that creates the object and returns it
# while __init__ just initializes the object passed as parameter
# you rarely use __new__, except when you want to control how the object
# is created.
# here the created object is the class, and we want to customize it
# so we override __new__
# you can do some stuff in __init__ too if you wish
# some advanced use involves overriding __call__ as well, but we won't
# see this
def __new__(upperattr_metaclass, future_class_name,
future_class_parents, future_class_attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in future_class_attrs.items()
}
return type(future_class_name, future_class_parents, uppercase_attrs)
Réécrivons ce qui précède, mais avec des noms de variables plus courts et plus réalistes maintenant que nous savons ce qu'ils signifient :
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in attrs.items()
}
return type(clsname, bases, uppercase_attrs)
Vous avez peut-être remarqué l'argument supplémentaire cls
. Il n'y a rien de spécial à ce sujet : __new__
reçoit toujours la classe dans laquelle elle est définie, comme premier paramètre. Tout comme vous avez self
pour les méthodes ordinaires qui reçoivent l'instance comme premier paramètre, ou la classe de définition pour les méthodes de classe.
Mais ce n'est pas une bonne méthode de fonctionnement opérationnel. Nous appelons type
directement et nous ne surchargeons pas ou n'appelons pas la méthode d'évaluation du parent. __new__
. Faisons plutôt cela :
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in attrs.items()
}
return type.__new__(cls, clsname, bases, uppercase_attrs)
Nous pouvons le rendre encore plus propre en utilisant super
ce qui facilitera l'héritage (car oui, vous pouvez avoir des métaclasses, héritant de métaclasses, héritant de type) :
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, attrs):
uppercase_attrs = {
attr if attr.startswith("__") else attr.upper(): v
for attr, v in attrs.items()
}
return super(UpperAttrMetaclass, cls).__new__(
cls, clsname, bases, uppercase_attrs)
Oh, et dans python 3 si vous faites cet appel avec des arguments de mots-clés, comme ceci :
class Foo(object, metaclass=MyMetaclass, kwarg1=value1):
...
Il se traduit par ceci dans la métaclasse pour l'utiliser :
class MyMetaclass(type):
def __new__(cls, clsname, bases, dct, kwargs1=default):
...
C'est ça. Il n'y a vraiment rien de plus à propos des métaclasses.
La raison derrière la complexité du code utilisant les métaclasses n'est pas à cause des métaclasses, c'est parce que vous utilisez généralement les métaclasses pour faire des trucs tordus en s'appuyant sur l'introspection, en manipulant l'héritage, les variables telles que __dict__
etc.
En effet, les métaclasses sont particulièrement utiles pour faire de la magie noire, et donc des choses compliquées. Mais en elles-mêmes, elles sont simples :
- intercepter une création de classe
- modifier la classe
- retourne la classe modifiée
Pourquoi utiliser des classes de métaclasses au lieu de fonctions ?
Depuis __metaclass__
peut accepter n'importe quel callable, pourquoi utiliser une classe puisque c'est évidemment plus compliqué ?
Il y a plusieurs raisons de le faire :
- L'intention est claire. Quand vous lisez
UpperAttrMetaclass(type)
vous savez ce qui va suivre
- Vous pouvez utiliser OOP. Les métaclasses peuvent hériter de métaclasses, remplacer les méthodes des parents. Les métaclasses peuvent même utiliser des métaclasses.
- Les sous-classes d'une classe seront des instances de sa métaclasse si vous avez spécifié une métaclasse-classe, mais pas avec une métaclasse-fonction.
- Vous pouvez mieux structurer votre code. On n'utilise jamais les métaclasses pour quelque chose d'aussi trivial que l'exemple ci-dessus. C'est généralement pour quelque chose de compliqué. Avoir la possibilité de faire plusieurs méthodes et de les regrouper dans une classe est très utile pour rendre le code plus facile à lire.
- Vous pouvez accrocher
__new__
, __init__
et __call__
. Ce qui vous permettra de faire des choses différentes, même si d'habitude vous pouvez faire tout cela en __new__
, certaines personnes sont simplement plus à l'aise en utilisant __init__
.
- On appelle ça des métaclasses, bon sang ! Ça doit vouloir dire quelque chose !
Pourquoi utiliser des métaclasses ?
Maintenant la grande question. Pourquoi utiliseriez-vous une obscure fonctionnalité sujette aux erreurs ?
Eh bien, en général, non :
Les métaclasses sont une magie plus profonde qui 99% des utilisateurs ne devraient jamais s'en préoccuper. Si vous vous demandez si vous en avez besoin, vous ne le faites pas (les personnes qui ont réellement ont besoin d'elles savent avec certitude qu'elles qu'ils en ont besoin et n'ont pas besoin d'une d'une explication sur le pourquoi).
Tim Peters, gourou de Python
Le principal cas d'utilisation d'une métaclasse est la création d'une API. L'ORM de Django en est un exemple typique. Il vous permet de définir quelque chose comme ceci :
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
Mais si tu fais ça :
person = Person(name='bob', age='35')
print(person.age)
Il ne retournera pas un IntegerField
objet. Il retournera un int
et peut même les prendre directement dans la base de données.
Cela est possible parce que models.Model
définit __metaclass__
et il utilise une certaine magie qui va transformer le Person
que vous venez de définir avec de simples déclarations en un crochet complexe vers un champ de la base de données.
Django fait paraître simple quelque chose de complexe en exposant une API simple. et en utilisant des métaclasses, recréant le code de cette API pour faire le vrai travail dans les coulisses.
Le dernier mot
Tout d'abord, vous savez que les classes sont des objets qui peuvent créer des instances.
En fait, les classes sont elles-mêmes des instances. De métaclasses.
>>> class Foo(object): pass
>>> id(Foo)
142630324
Tout est un objet en Python, et ils sont tous soit des instances de classes soit des instances de métaclasses.
Sauf pour type
.
type
est en fait sa propre métaclasse. Ce n'est pas quelque chose que vous pourriez reproduire en Python pur, et c'est fait en trichant un peu au niveau de l'implémentation. d'implémentation.
Deuxièmement, les métaclasses sont compliquées. Vous ne voudrez peut-être pas les utiliser pour pour des modifications de classes très simples. Vous pouvez modifier les classes en utilisant deux techniques différentes :
Dans 99% des cas où vous avez besoin d'une altération de classe, il est préférable d'utiliser celles-ci.
Mais dans 98 % des cas, il n'est pas nécessaire de modifier la classe du tout.