Licence CC BY-NC-ND, Thierry Parmentelat & Arnaud Legout

from IPython.display import HTML
HTML(filename="_static/style.html")

métaclasses#

on a vu dans ce chapitre, et spécifiquement ici les mécanismes généraux de l’héritage en Python
mais ici encore, c’est la version simple des choses, car en bon langae objet qui se respecte,
Python nous permet aussi .. de modifier les règles de base de l’héritage (entre autres) !
c’est le propos des métaclasses, que nous allons étudier maintenant

à quoi ça sert ?#

hum, bonne question ! en réalité on peut s’en servir pour énormément de choses! (même si, bien souvent il y a d’autres alternatives, dans la ménagerie de concepts Python, pour faire ces choses sans passer par les métaclasses…)
mais bon en tous cas c’est intéressant du point de vue conceptuel !

un prétexte: le singleton#

pour quand même travailler sur une application concrète, nous allons illustrer le concept en implémentant la notion de singleton

c’est quoi ? on parle de singleton lorsqu’on veut qu’une classe n’existe qu’en un seul exemplaire dans l’application; comme un espèce d’objet global mais qui serait proprement partagé
par exemple: la configuration; ou une classe API pour dialoguer avec un autre programme; ou bien un cache pour ranger des données…

mais avant d’en arriver là nous allons devoir faire un peu de théorie…

type et object#

jusqu’ici nous avons vu que

  • tout est objet en Python

  • tout objet a un type

  • une classe est un type - à partir d’ici on ne fera plus la distinction entre ces deux termes

  • une classe a aussi possiblement une ou plusieurs super-classes

relation entre objets et types#

du coup on peut s’intéresser à la relation binaire entre les objets:

  • \(X\) est une instance de \(Y\) (\(Y\) est le type de \(X\))

# et pour ça on va se créer quelques classes et objets

class Class: pass

class SubClass(Class): pass

instance = SubClass()

pour introspecter la relation “est une instance de”, c’est facile, on utilise type() !

# remontons la relation en partant de vector

type(instance), type(SubClass), type(Class), type(type)
(__main__.SubClass, type, type, type)

ce qui nous donne un premier élément de réponse:

  • un instance a comme type la classe qui l’a créée

  • une classe a comme type .. type

on peut créer une classe avec type()

si vous êtes attentifs: on a dit qu’un objet est créé par son type (sa classe) considérée comme une usine à objets
et donc puisque la classe de Vector est type, ça signifie qu’on aurait pu créer Vector en appelant type() ?

eh bien oui, en effet !
on a souvent utilisé type() avec un seul paramètre, mais il existe aussi une forme avec 3 paramètres, qui permet de créer une classe !

ce qui nous donne le diagramme suivant

_images/objects-types.svg

Quiz

que donne à votre avis type(int) ? et type(object)

héritage entre classes#

maintenant regardons quelle est la relation entre classes et super-classes:

  • \(X\) hérite directement \(Y\) (\(X\) a comme classe de base \(Y\))

la question ne se pose pas à propos de vector (seules les classes ont des super-classes)
mais pour les autres on obtient ceci - en utilisant l’attribut spécial __bases__

# the base classes of our classes

SubClass.__bases__, Class.__bases__
((__main__.Class,), (object,))
# the base classes of the builtin classes

object.__bases__, type.__bases__
((), (object,))

donc on obtient, si on superpose les deux relations:

_images/objects-types-bases.svg

comment les objets sont-ils créés ?#

une fois que ceci est bien clair, voyons un peu plus en détail comment les objets sont créés en Python
c’est-à-dire quand j’appelle par exemple Class(0):

  • l’objet Class est donc un callable, on va chercher son attribut __call__

  • comme il s’agit d’une utilisation implicite de dunder, on ne cherche pas dans l’espace de nom de Person, mais dans sa classe

  • or on vient de le voir, sa classe c’est type; effectivement il existe une méthode type.__call__() qui fait - en gros - ceci (credits)

# the actual code is in C, but a pseudo implementation
# of the type class in Python could look like this

class type:
    # when calling Class(0), we will call this method with
    # cls = Class
    # args = (0,)
    # kwargs = {}
    def __call__(cls, *args, **kwarg):

        # ... a few things could possibly be done to cls here... maybe... or maybe not...

        # then we call cls.__new__() to get a new object
        # think of this as a class method (the object has not been created yet...)
        obj = cls.__new__(cls, *args, **kwargs)

        # ... a few things done to obj here... maybe... or not...

        # then we call obj.__init__()
        obj.__init__(*args, **kwargs)

        # ... maybe a few more things done to obj here

        # then we return obj
        return obj

c’est-à-dire en français, on crée un objet en passant par deux étapes, qui sont chacune liée à une dunder:

  • __new__() une méthode de classe (car on n’a pas encore créé l’objet) dans l’esprit, elle est chargée d’allouer la mémoire

  • __init__()une méthode usuelle chargée d’initialiser l’objet

le cas d’un objet usuel#

on peut donc poursuivre notre analyse, on est toujours en train d’évaluer Class(0)

au moment d’appeler __new__ on la cherche à partir de l’objet Class
qui en général n’a pas redéfini cette méthode
on

WIP

xxx

pour en savoir plus#

cette section de la doc officielle donne tous les détails

https://docs.python.org/3/reference/datamodel.html#customizing-class-creation