6618 votes

Que sont les métaclasses en Python ?

En Python, que sont les métaclasses et à quoi servent-elles ?

150voto

Je pense que l'introduction à la programmation par métaclasses de l'ONLamp est bien écrite et donne une très bonne introduction au sujet, même si elle date déjà de plusieurs années.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html (archivé à https://web.archive.org/web/20080206005253/http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html )

En bref : une classe est un plan pour la création d'une instance, une métaclasse est un plan pour la création d'une classe. Il est facile de voir qu'en Python, les classes doivent également être des objets de première classe pour permettre ce comportement.

Je n'en ai jamais écrit moi-même, mais je pense que l'une des plus belles utilisations des métaclasses se trouve dans l'exemple suivant Cadre de travail Django . Les classes de modèles utilisent une approche métaclasse pour permettre un style déclaratif d'écriture de nouveaux modèles ou de classes de formulaires. Pendant que la métaclasse crée la classe, tous les membres ont la possibilité de personnaliser la classe elle-même.

La chose qui reste à dire est : Si vous ne savez pas ce que sont les métaclasses, la probabilité que vous n'en auront pas besoin est de 99 %.

137voto

Aaron Hall Points 7381

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>})

103voto

Ethan Furman Points 12683

Mise à jour de Python 3

Il existe (à ce stade) deux méthodes clés dans une métaclasse :

  • __prepare__ et
  • __new__

__prepare__ vous permet de fournir un mappage personnalisé (tel qu'un OrderedDict ) qui sera utilisé comme espace de nom pendant la création de la classe. Vous devez retourner une instance de l'espace de noms que vous avez choisi. Si vous n'implémentez pas __prepare__ une normale dict est utilisé.

__new__ est responsable de la création/modification effective de la classe finale.

Une métaclasse de base, qui ne fait rien de plus, ressemblerait à.. :

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

Un exemple simple :

Supposons que vous souhaitiez exécuter un code de validation simple sur vos attributs - par exemple, il doit toujours s'agir d'une valeur int ou un str . Sans métaclasse, votre classe ressemblerait à quelque chose comme :

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

Comme vous pouvez le constater, vous devez répéter deux fois le nom de l'attribut. Cela rend les fautes de frappe possibles ainsi que les bugs irritants.

Une simple métaclasse peut résoudre ce problème :

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

Voici à quoi ressemblerait la métaclasse (sans l'utilisation de __prepare__ puisqu'il n'est pas nécessaire) :

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

Une série d'échantillons :

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

produit :

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

Note : Cet exemple est assez simple, il aurait également pu être accompli avec un décorateur de classe, mais on peut supposer qu'une métaclasse réelle ferait beaucoup plus.

La classe "ValidateType" à titre de référence :

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value

1 votes

Notez que depuis python 3.6, vous pouvez utiliser __set_name__(cls, name) dans le descripteur ( ValidateType ) pour définir le nom dans le descripteur ( self.name et dans ce cas aussi self.attr ). Ceci a été ajouté pour ne pas avoir à se plonger dans les métaclasses pour ce cas d'utilisation commun spécifique (voir PEP 487).

89voto

mike Points 3565

Rôle d'une métaclasse __call__() lors de la création d'une instance de classe

Si vous avez fait de la programmation Python pendant plus de quelques mois, vous finirez par tomber sur du code qui ressemble à ceci :

# define a class
class SomeClass(object):
    # ...
    # some definition here ...
    # ...

# create an instance of it
instance = SomeClass()

# then call the object as if it's a function
result = instance('foo', 'bar')

Ce dernier point est possible lorsque vous mettez en œuvre l'option __call__() méthode magique sur la classe.

class SomeClass(object):
    # ...
    # some definition here ...
    # ...

    def __call__(self, foo, bar):
        return bar + foo

Le site __call__() est invoquée lorsqu'une instance d'une classe est utilisée comme appelable. Mais comme nous l'avons vu dans les réponses précédentes, une classe elle-même est une instance d'une métaclasse, donc lorsque nous utilisons la classe comme appelable (c'est-à-dire lorsque nous créons une instance de celle-ci), nous appelons en fait sa métaclasse'. __call__() méthode. À ce stade, la plupart des programmeurs Python sont un peu perdus car on leur a dit que lorsqu'ils créent une instance comme ceci instance = SomeClass() vous appelez son __init__() méthode. Certains qui ont creusé un peu plus profondément savent qu'avant __init__() il y a __new__() . Eh bien, aujourd'hui, une autre couche de vérité est révélée, avant __new__() il y a la métaclasse __call__() .

Étudions la chaîne d'appel des méthodes du point de vue spécifique de la création d'une instance d'une classe.

Il s'agit d'une métaclasse qui enregistre exactement le moment avant la création d'une instance et le moment où elle est sur le point de la retourner.

class Meta_1(type):
    def __call__(cls):
        print "Meta_1.__call__() before creating an instance of ", cls
        instance = super(Meta_1, cls).__call__()
        print "Meta_1.__call__() about to return instance."
        return instance

Voici une classe qui utilise cette métaclasse

class Class_1(object):

    __metaclass__ = Meta_1

    def __new__(cls):
        print "Class_1.__new__() before creating an instance."
        instance = super(Class_1, cls).__new__(cls)
        print "Class_1.__new__() about to return instance."
        return instance

    def __init__(self):
        print "entering Class_1.__init__() for instance initialization."
        super(Class_1,self).__init__()
        print "exiting Class_1.__init__()."

Et maintenant, nous allons créer une instance de Class_1

instance = Class_1()
# Meta_1.__call__() before creating an instance of <class '__main__.Class_1'>.
# Class_1.__new__() before creating an instance.
# Class_1.__new__() about to return instance.
# entering Class_1.__init__() for instance initialization.
# exiting Class_1.__init__().
# Meta_1.__call__() about to return instance.

Observez que le code ci-dessus ne fait rien de plus que de consigner les tâches. Chaque méthode délègue le travail réel à l'implémentation de son parent, conservant ainsi le comportement par défaut. Puisque type est Meta_1 de la classe mère ( type étant la métaclasse parent par défaut) et en considérant la séquence d'ordonnancement de la sortie ci-dessus, nous avons maintenant un indice de ce que serait la pseudo-implémentation de type.__call__() :

class type:
    def __call__(cls, *args, **kwarg):

        # ... maybe a few things done to cls here

        # then we call __new__() on the class to create an instance
        instance = cls.__new__(cls, *args, **kwargs)

        # ... maybe a few things done to the instance here

        # then we initialize the instance with its __init__() method
        instance.__init__(*args, **kwargs)

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

        # then we return it
        return instance

Nous pouvons voir que la métaclasse __call__() est celle qui est appelée en premier. Elle délègue ensuite la création de l'instance à l'instance de la classe. __new__() et l'initialisation de la méthode __init__() . C'est aussi celui qui renvoie finalement l'instance.

De ce qui précède, il ressort que la métaclasse __call__() a également la possibilité de décider si l'appel à la fonction Class_1.__new__() ou Class_1.__init__() seront finalement réalisées. Au cours de son exécution, elle pourrait en fait renvoyer un objet qui n'a été touché par aucune de ces méthodes. Prenons par exemple cette approche du modèle singleton :

class Meta_2(type):
    singletons = {}

    def __call__(cls, *args, **kwargs):
        if cls in Meta_2.singletons:
            # we return the only instance and skip a call to __new__()
            # and __init__()
            print ("{} singleton returning from Meta_2.__call__(), "
                   "skipping creation of new instance.".format(cls))
            return Meta_2.singletons[cls]

        # else if the singleton isn't present we proceed as usual
        print "Meta_2.__call__() before creating an instance."
        instance = super(Meta_2, cls).__call__(*args, **kwargs)
        Meta_2.singletons[cls] = instance
        print "Meta_2.__call__() returning new instance."
        return instance

class Class_2(object):

    __metaclass__ = Meta_2

    def __new__(cls, *args, **kwargs):
        print "Class_2.__new__() before creating instance."
        instance = super(Class_2, cls).__new__(cls)
        print "Class_2.__new__() returning instance."
        return instance

    def __init__(self, *args, **kwargs):
        print "entering Class_2.__init__() for initialization."
        super(Class_2, self).__init__()
        print "exiting Class_2.__init__()."

Observons ce qui se passe lorsqu'on essaie à plusieurs reprises de créer un objet de type Class_2

a = Class_2()
# Meta_2.__call__() before creating an instance.
# Class_2.__new__() before creating instance.
# Class_2.__new__() returning instance.
# entering Class_2.__init__() for initialization.
# exiting Class_2.__init__().
# Meta_2.__call__() returning new instance.

b = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

c = Class_2()
# <class '__main__.Class_2'> singleton returning from Meta_2.__call__(), skipping creation of new instance.

a is b is c # True

2 votes

C'est un bon complément à la "réponse acceptée" précédemment votée. Elle fournit des exemples à méditer pour les codeurs intermédiaires.

71voto

Craig Points 1136

Une métaclasse est une classe qui indique comment (une) autre classe doit être créée.

C'est un cas où j'ai vu la métaclasse comme une solution à mon problème : J'avais un problème vraiment compliqué, qui aurait probablement pu être résolu autrement, mais j'ai choisi de le résoudre en utilisant une métaclasse. En raison de la complexité, c'est l'un des rares modules que j'ai écrits où les commentaires du module dépassent la quantité de code qui a été écrite. Le voici...

#!/usr/bin/env python

# Copyright (C) 2013-2014 Craig Phillips.  All rights reserved.

# This requires some explaining.  The point of this metaclass excercise is to
# create a static abstract class that is in one way or another, dormant until
# queried.  I experimented with creating a singlton on import, but that did
# not quite behave how I wanted it to.  See now here, we are creating a class
# called GsyncOptions, that on import, will do nothing except state that its
# class creator is GsyncOptionsType.  This means, docopt doesn't parse any
# of the help document, nor does it start processing command line options.
# So importing this module becomes really efficient.  The complicated bit
# comes from requiring the GsyncOptions class to be static.  By that, I mean
# any property on it, may or may not exist, since they are not statically
# defined; so I can't simply just define the class with a whole bunch of
# properties that are @property @staticmethods.
#
# So here's how it works:
#
# Executing 'from libgsync.options import GsyncOptions' does nothing more
# than load up this module, define the Type and the Class and import them
# into the callers namespace.  Simple.
#
# Invoking 'GsyncOptions.debug' for the first time, or any other property
# causes the __metaclass__ __getattr__ method to be called, since the class
# is not instantiated as a class instance yet.  The __getattr__ method on
# the type then initialises the class (GsyncOptions) via the __initialiseClass
# method.  This is the first and only time the class will actually have its
# dictionary statically populated.  The docopt module is invoked to parse the
# usage document and generate command line options from it.  These are then
# paired with their defaults and what's in sys.argv.  After all that, we
# setup some dynamic properties that could not be defined by their name in
# the usage, before everything is then transplanted onto the actual class
# object (or static class GsyncOptions).
#
# Another piece of magic, is to allow command line options to be set in
# in their native form and be translated into argparse style properties.
#
# Finally, the GsyncListOptions class is actually where the options are
# stored.  This only acts as a mechanism for storing options as lists, to
# allow aggregation of duplicate options or options that can be specified
# multiple times.  The __getattr__ call hides this by default, returning the
# last item in a property's list.  However, if the entire list is required,
# calling the 'list()' method on the GsyncOptions class, returns a reference
# to the GsyncListOptions class, which contains all of the same properties
# but as lists and without the duplication of having them as both lists and
# static singlton values.
#
# So this actually means that GsyncOptions is actually a static proxy class...
#
# ...And all this is neatly hidden within a closure for safe keeping.
def GetGsyncOptionsType():
    class GsyncListOptions(object):
        __initialised = False

    class GsyncOptionsType(type):
        def __initialiseClass(cls):
            if GsyncListOptions._GsyncListOptions__initialised: return

            from docopt import docopt
            from libgsync.options import doc
            from libgsync import __version__

            options = docopt(
                doc.__doc__ % __version__,
                version = __version__,
                options_first = True
            )

            paths = options.pop('<path>', None)
            setattr(cls, "destination_path", paths.pop() if paths else None)
            setattr(cls, "source_paths", paths)
            setattr(cls, "options", options)

            for k, v in options.iteritems():
                setattr(cls, k, v)

            GsyncListOptions._GsyncListOptions__initialised = True

        def list(cls):
            return GsyncListOptions

        def __getattr__(cls, name):
            cls.__initialiseClass()
            return getattr(GsyncListOptions, name)[-1]

        def __setattr__(cls, name, value):
            # Substitut option names: --an-option-name for an_option_name
            import re
            name = re.sub(r'^__', "", re.sub(r'-', "_", name))
            listvalue = []

            # Ensure value is converted to a list type for GsyncListOptions
            if isinstance(value, list):
                if value:
                    listvalue = [] + value
                else:
                    listvalue = [ None ]
            else:
                listvalue = [ value ]

            type.__setattr__(GsyncListOptions, name, listvalue)

    # Cleanup this module to prevent tinkering.
    import sys
    module = sys.modules[__name__]
    del module.__dict__['GetGsyncOptionsType']

    return GsyncOptionsType

# Our singlton abstract proxy class.
class GsyncOptions(object):
    __metaclass__ = GetGsyncOptionsType()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X