metaclass – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 8. http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-8/ http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-8/#comments Tue, 02 Jul 2013 07:31:26 +0000 http://sametmax.com/?p=6524 8ème et dernier chapitre sur la programmation orientée objet en Python. Nous allons voir l’ultime réalité, le secret cosmique de nirvana Pythonique, le truc dont personne ne se sert avant d’avoir au moins codé 3 moteurs de blogs, un bot twitter et une IA de real doll.

J’ai nommé…

Les métaclasses.

Comme tous les articles sur la question, je commence avec la citation standard :

Les métaclasses sont une magie trop obscure pour que 99% des utilisateurs s’en préoccupe. Si vous vous demandez si vous en avez besoin, ce n’est pas le cas (les gens qui en ont vraiment besoin le savent avec certitude et n’ont pas besoin d’explications sur la raison).

Tim Peters, les mec qui a écrit le PEP20.

En résumé, cet article ne vous servira probablement pas. Je me suis servi, en 10 ans, deux fois des métaclasses dans ma vie. C’est pour le sport, quoi.

Pré-requis:

  • Avoir lu la partie précédente et bien tout compris. Mais alors bien tout. Si il subsiste un doute dans votre esprit, relisez toute la série car vous ne vous en sortirez pas.
  • Notamment, avoir bien pigé le principe de object, car les metaclasses ne fonctionnent qu’avec les New Style classes.
  • Comprendre le principe de références, callable, lambdas et dictionnaires en Python. En fait à peu prêt tout sauf les métaclasses parce que ce truc est ce que vous voulez apprendre en dernier de toute façon.
  • Être bien reposé et avoir du temps devant soi. Pas la peine de lire en diagonal. Vraiment.

Musique.

Au début, il y avait les objets

Et Dieu Guido vit que cela était bon, et qu’il n’y avait pas de raison de ne pas se marrer un bon coup, alors il décida que les classes, qui servaient à fabriquer les objets, seraient aussi des objets.

Dans la plupart des langages, une classe est vraiment juste un plan pour produire un objet, un bout de code, une syntaxe élaborée pour dire “voici ma classe”. On peut l’imaginer comme cela en Python également, et vivre très heureux et avoir beaucoup de bons moments :

>>> class CreateurDObject(object):
...     pass
...
>>> mon_objet = CreateurDObject()
>>> print(mon_objet)
<__main__.CreateurDObject object at 0x1c22fd0>

Sauf qu’en Python, les classes sont aussi des objets.

Je vous laisse maturer ça 1 minute. Prenez une inspiration.

Quand l’interpréteur Python lit le mot class, il crée un objet. Dans le code ci-dessus, en mémoire, Python crée un objet CreateurDObject.

Cet objet est une classe, il permet de créer d’autres objets – ses instances – mais ça reste un objet. Et comme tous les objets…

On peut l’assigner à une variable :

>>> ReferenceACreateurDObjet = CreateurDObject
>>> ReferenceACreateurDObjet()
<__main__.CreateurDObject object at 0x1c320d0>

On peut le passer en paramètre :

>>> def afficher_une_class(cls):
...     print("Hey, on m'a passé la classe %s" % cls)
...
>>> afficher_une_class(CreateurDObject)
Hey, on m'a passé la classe 

On peut lui ajouter des attributs à la volée :

>>> CreateurDObject.nouvel_attribut = 'nouvelle valeur'
>>> hasattr(CreateurDObject, 'nouvel_attribut')
True
>>> CreateurDObject.nouvel_attribut
u'nouvelle valeur'

Le 7eme jour, on se faisait grave chier alors on a créé des classes dynamiquement

Peut être vous souvenez-vous qu’on peut créer des fonctions à la volée ?

Parce qu’on peut faire pareil avec les classes :

>>> def fabriquer_une_class(nom):
...     if nom == 'bulbizarre':
...         class Bulbizarre(object):
...             pass
...         return Bulbizarre # je retourne Bulbizarre, PAS Bulbizarre()
...     else:
...         class Salameche(object): # qui prenait carapuce, sérieux ?
...             pass
...         return Salameche
>>> Pokemon = fabriquer_une_class('autre')
>>> type(Pokemon) # Pokemon est une CLASSE, pas une instance

>>> nouveau_pokemon = Pokemon() # la je crée une instance
>>> type(nouveau_pokemon)

Une classe n’est pas quelque chose de figé dans le marbre, comme tout le reste en Python, on peut les fabriquer dynamiquement, les modifier en cours de route, les malmener, etc.

Aucun pokemon n’a cependant été blessé pendant la rédaction de cet article. Tiens je me demande si il y a des sites zoophiles spécialisés dans les Pokemons. Qu’est-ce que je raconte ? Évidement qu’il y en a. D’ailleurs Pikachu est comme une shocking flesh torch avec piles incluses quand on y pense. Ou un appareil à abdos. Il faut que je fasse des abdos, j’ai pris du bide. Heu… où j’en étais ?

Ah oui. Classes. Dynamiques.

Comme toutes les opérations courantes idées à la con, Python vous permet de faire ça complètement à la main. Maintenant vient la troisième Révélation Des Métaclasses : la fonction type() a deux usages.

Dans une première vie type() est une fonction ordinaire, elle sert à retourner le type d’un objet :

>>> print type(1)

>>> print type("1")

>>> print type(CreateurDObject)

>>> print type(CreateurDObject())

Et propose à sa logeuse de descendre ses poubelles. Mais elle a une autre vie électronique, elle est aussi capable de créer une classe.

Oui c’est très con d’avoir choisit la même PUTAIN DE FONCTION pour faire deux trucs qui n’ont rien à voir. Mais les deux usages ont un avenir et il va falloir faire avec, se caler ça où je pense et oublier votre avocat.

Bref, ça marche comme ça :

type(nom de la classe, tuple des parents de la classe, dictionnaire contenant les attributs)

Le nom de la classe, je pense que vous avez pigé.

Le tuple des parents, il peut être vide. C’est dans le cas où vous souhaitez un héritage : vous pouvez passer des références aux classes parentes.

Le dictionnaire est assez simple : chaque clé est un nom d’attribut, chaque valeur est une valeur d’attribut.

Exemple, cette classe :

>>> class Pokeball(object):
...     pass

Peut se créer ainsi :

>>> Pokeball = type('Pokeball', (), {})

Et s’utiliser tout pareil :

>>> print(Pokeball)

>>> print(Pokeball())
<__main__.Pokeball object at 0x1c32450>

Vous aurez noté que le nom de classe passé en paramètre est le même que celui de la variable qui va recevoir la classe ainsi créée. Oui, ils peuvent être différents. Non, ça ne sert à rien.

Et je sais que certains auraient préféré MyLittlePoney, mais je fais avec ma maigre culture G. Faudrait que je tente avec des noms de Pogs.

Un petit exemple avec des attributs :

>>> class Pokeball(object):
...     couleurs = ('rouge', 'blanc')

Ce qui donne :

>>> Pokeball = type('Pokeball', (), {'couleurs': ('rouge', 'blanc')})
>>> Pokeball.couleurs
(u'rouge', u'blanc')

Étant donné que les méthodes sont des attributs…

>>> class Pokeball(object):
  ...     def attraper(self):
  ...         print("ratééééééééééééé")
  ...       

Peut se traduire par :

>>> def out(): print "ratéééééééééééé"
...
>>> Pokeball = type('Pokeball', (), {'attraper': out})
>>> Pokeball().attraper()
ratéééééééééééé

Au passage, toute classe créée par type hérite automatiquement de object, pas besoin de le préciser.

Petite démo avec de l’héritage :

>>> class Masterball(Pokeball):
...     def attraper(self):
...         print("yeahhhhhhhhhh")
...         

Se transforme en :

>>> def out(): print "yeahhhhhhhhhh"
...
>>> Masterball = type('Masterball', (Pokeball,), {'attraper': out})
>>> Masterball().attraper()
yeahhhhhhhhhh

Je répète les Révélations Divines :

  1. Les classes sont des objets.
  2. On peut créer les classes à la volée.
  3. type() permet de créer une classe manuellement.
  4. Keanu Reeves est un mauvais acteur.

Vous voyez où je veux en venir, là, non ?

Et le tout puissant déclara : tu créeras des classes avec des classes

Et tu n’aimeras pas ça. Alors au début tu le feras avec des fonctions parce que c’est plus facile.

Une métaclasse, c’est seulement le nom du “truc” qui fabrique une classe. C’est tout.

C’est pour ça qu’on appelle cela des métaclasses, les classes des classes.

Puisque les classes sont des objets ?

Vous suivez ?

Non ?

Ah.

o

En deux lignes alors :

UneClasse = MetaClass()
un_objet = UneClasse()

Mieux ?

Vous avez vu la fonction type() créer des classes ? En fait la fonction type() est une métaclasse. C’est la métaclasse dont Python se sert pour créer tous les objets classes, quand vous tapez le keyword class.

Il est facile de voir ça en regardant l’attribut __class__, qui indique quelle classe a servi à créer un objet :

>>> pokemon_capture = 149
>>> pokemon_capture.__class__

>>> nom = 'magicarp'
>>> nom.__class__

>>> def attrapez_les_presque_tous(): pass
>>> attrapez_les_presque_tous.__class__

>>> class Pokedex(object): pass
>>> gadget = Pokedex()
>>> gadget.__class__

Mais quelle est le __class__ de tout __class__ ?

>>> attrapez_les_presque_tous.__class__.__class__

>>> pokemon_capture.__class__.__class__

>>> nom.__class__.__class__

>>> gadget.__class__.__class__

Normalement c’est à ce moment là que vous avez la Grande Révélation.

Faites “Ahhhhhhhhhhhhhhhhh”.

Donc la métaclasse, c’est le machin qui fabrique les classes, c’est une factory de classe. type est la métaclasse utilisée par défaut, mais vous pensez bien, chers amis, qu’on peut fabriquer ses propres métaclasses. Sinon ça serait pas marrant.

Ça marche comme ça :

class UneClasse(object):
  __metaclass__ = votre métaclasse
  [ le reste du code de la classe ]

Quand Python va voir ça, il ne va pas créer la classe immédiatement, il va d’abord suivre la chaîne d’héritage pour trouver quelle métaclasse utiliser pour fabriquer la classe :

Il va d’abord vérifier si il y a __metaclass__ de déclarée. Si ce n’est pas le cas, il va chercher dans les parents si ils ont un attribut __metaclass__. Si ce n’est pas le cas, il va utiliser type. Si Python trouve __metaclass__, alors il utilisera son contenu à la place de type.

Dans les deux cas, il va collecter toutes les informations sur la classe (le nom, les parents, les attributs), et les passer en paramètres à type ou votre métaclasse, et récupérer le résultat. Puis, seulement à ce moment là, il enregistre votre classe en mémoire.

Prenez votre temps sur ces paragraphes, c’est un peu la clé de tout le tuto.

Bon, nouvelle révélation : une métaclasse n’a pas besoin d’être une classe.

Je sais, c’est idiot d’appeler un truc “métaclasse” si ça n’a pas besoin d’être une classe. Mais on parle de la fonctionnalité qui utilise type pour un truc qui n’a rien à voir. Ça devait être le jour du beaujolais nouveau quand ils ont introduit la feature.

En fait, une métaclasse peut être n’importe quel callable qui retourne une classe, donc une fonction toute conne fait très bien l’affaire.

Le but de la métaclasse, c’est d’intercepter la création de la classe afin de la modifier, et voici ce que ça donne :

# une métaclasse DOIT avoir la même signature que type() puisque
# Python va lui passer tout ça automatiquement en paramètre

def prefixer(nom, parents, attributs):
    """
        On va créer une métaclasse qui prend tous les noms de méthodes,
        et les préfixes du mot "attaque". Oui ça ne sert à rien. Mais
        les usages des métaclasses qui servent à quelque chose sont
        généralement très compliqués.
    """

    # On crée un nouveau dictionnaire d'attributs avec les noms préfixés
    # On fait gaffe à pas modifier les __méthodes_magique__.
    nouveaux_attributs = {}
    for nom, val in attributs.items():
        if not nom.startswith('__'):
            nouveaux_attributs['attaque_' + nom] = val
        else:
            nouveaux_attributs[nom] = val

    # On délègue la création de la classe à type() :
    return type(nom, parents, nouveaux_attributs)

L’utilisation est toute simple, on écrit une classe normale, et on lui rajoute __metaclass__:

>>> class Ronflex(object):

    __metaclass__ = prefixer

    def armure(self):
        print("defense +15")

    def dodo(self):
        print("zzzzzz")
>>> r = Ronflex()
>>> r.attaque_armure()
defense +15
>>> r.attaque_dodo()
zzzzzz
>>> r.dodo()
Traceback (most recent call last):
  File "", line 1, in 
    r.dodo()
AttributeError: 'armure' object has no attribute 'dodo'

Voilà, c’est à peu près tout ce qu’il y a à comprendre des métaclasses :

  1. On intercepte la création classe.
  2. On modifie les paramètres.
  3. On retourne la classe customisée.

Maintenant, la grande question : à quoi ça sert ?

Généralement, ça sert à faire de jolies APIs. Par exemple, les ORM (peewee, SQLAlchemy, l’ORM de Django) utilisent les métaclasses pour avoir un style déclaratif :

class Article(Model):
    titre = model.CharField()

Ici, Model va contenir une métaclasse, comme c’est un parent, la métaclasse sera aussi appelée pour Article, et elle peut donc agir pour modifier titre à la volée.

En effet, quand vous faites :

>>> art = Article.get(id=13)
>>> art.titre
u'Python pour les méca-scriptophiles'

Avec un ORM, vous récupérez la valeur de titre dans la base de données et non un CharField(). C’est le but : cacher des requêtes SQL et donner l’impression de manipuler des objets.

Ici le rôle de la métaclasse, c’est de prendre tous les attributs de types xxxField(), et modifier la classe pour qu’accéder à l’attribut fasse la requête voulue à la base de données.

On peut aussi utiliser les métaclasses pour faire des vérifications : s’assurer que la classe n’utilise pas un nom que vous voulez réserver, ou qu’elle contient bien un attribut.

Il y a même des implémentations d’interfaces (ou plutôt de l’équivalent des classes abstraites en Java) pour Python utilisant les métaclasses. Je ne suis pas convaincu par leur utilité, mais c’est possible.

Soyez prophètes

C’est pas le tout, mais maintenant que vous avez compris, il est temps de limer les bords pour que vous alliez porter la bonne parole.

Donc déjà, bon à savoir : metaclass est un paramètre de classe en Python 3. Ca donne ça:

class DePython3(metaclass=votre_metaclasse)

Ensuite, il y des conventions de nommage. Tout comme on nomme self le premier paramètre des méthodes, et *args / **kwargs les paramètres dynamiques, les paramètres des métaclasses ont des noms conventionels. La métaclasse précédente s’écrirait donc plus proprement :

def prefixer(name, bases, dct):
    nouveaux_attributs = {}
    for nom, val in dct.items():
        if not nom.startswith('__'):
            nouveaux_attributs['attaque_' + nom] = val
        else:
            nouveaux_attributs[nom] = val

    # On délègue la création de la classe à type() :
    return type(name, bases, nouveaux_attributs)

Enfin, souvenez-vous que les métaclasses ne fonctionnent qu’avec les New Style classes, donc celles qui héritent de object.

Par ailleurs, vous croiserez peut être des fois __metaclass__ en plein milieu d’un module (pas dans une classe donc). C’est une vieille façon de faire, qui affecte toutes les classes du module. Ce n’est plus recommandé.

Terminons sur une version de métaclasse qui fait la même chose que prefixer(), mais sous forme de classe. Parce que sinon à quoi ça sert que ça s’appelle une métaclasse, hein ?

# Oui, on peut hériter de type(), car c'est une classe, enfin une métaclasse.
# Mais pas sous forme de fonction. Sous forme de classe. Mais son nom
# est en minuscule, comme celui des classes int() et str(). Qui ne sont
# pas des métaclasses. Souvenez-vous : Beaujolais.
class Prefixer(type):

    # __new__ est le vrai constructeur en Python, et il retourne l'instance
    # en cours d'une classe. Or, qu'elle est l'instance d'une métaclasse ?
    # Une classe !
    def __new__(cls, name, bases, dct):

        nouveaux_attributs = {}
        for nom, val in dct.items():
            if not nom.startswith('__'):
                nouveaux_attributs['attaque_' + nom] = val
            else:
                nouveaux_attributs[nom] = val

        # On délègue la création de la classe à son parent.
        # Notez que cls doit être passé de bout en bout, ce qui n'est pas
        # le cas d'habitude avec 'self'
        return super(Prefixer, cls).__new__(cls, name, bases, nouveaux_attributs)

Et ça s’utilise pareil

>>> class Ronflex(object):
    __metaclass__ = Prefixer
    def armure(self):
        print("defense +15")
    def dodo(self):
        print("zzzzzz")
...
>>> r = Ronflex()
>>> r.attaque_dodo()
zzzzzz
>>> r.attaque_armure()
defense +15
>>> r.dodo()
Traceback (most recent call last):
  File "", line 1, in 
    r.dodo()
AttributeError: 'Ronflex' object has no attribute 'dodo'

Pourquoi utiliser une classe plutôt qu’une fonction alors que c’est vraiment plus compliqué ?

Et bien l’avantage d’utiliser une classe est qu’elle peut avoir des attributs et hériter. Car bien entendu, on peut avoir des metaclasses, qui héritent de metaclasses, et ainsi de suite, jusqu’à ce que du Lisp paraisse avoir du sens en comparaison.

On peut même faire plus vicieux et utiliser __call__ au lieu de __new__:

# on hérite plus de type, rien à foutre
class Prefixer(object):

    def __init__(self, prefix='attaque'):
        self.prefix = prefix

    # __call__ est la méthode appelée automatiquement quand on ajoute () après
    # un nom d'objet. Ca permet de rendre un objet callable. En gros, ça nous
    # permet d'utiliser une instance de Prefixer() comme une fonction
    def __call__(self, name, bases, dct):

        nouveaux_attributs = {}
        for nom, val in dct.items():
            if not nom.startswith('__'):
                nouveaux_attributs[self.prefix + nom] = val
            else:
                nouveaux_attributs[nom] = val

        return type(name, bases, nouveaux_attributs)

Et c’est sympa car du coup on peut passer des paramètres à notre métaclasse :

>>> class Ronflex(object):
    __metaclass__ = Prefixer(prefix='tatayoyo_')
    def armure(self):
        print("defense +15")
    def dodo(self):
        print("zzzzzz")
...         
>>> r = Ronflex()
>>> r.tatayoyo_dodo()
zzzzzz

Voilà, vous avez vu le visage de Dieu en face. C’est comme ça qu’il fabrique le monde.

Ah mais attendez ? Si type() est la métaclasse de tous les objets ? Qu’est-ce qui et la métaclasse de type() ?

>>> type.__class__

Je vous laisse méditer sur cette découverte métaphysique.

]]>
http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-8/feed/ 31 6524