Que sont les métaclasses en Python?

Réponses:

2869

Une métaclasse est la classe d'une classe. Une classe définit le comportement d'une instance de la classe (c'est-à-dire un objet) tandis qu'une métaclasse définit le comportement d'une classe. Une classe est une instance d'une métaclasse.

Alors qu'en Python, vous pouvez utiliser des callables arbitraires pour les métaclasses (comme le montre Jerub ), la meilleure approche est d'en faire une classe réelle elle-même. typeest la métaclasse habituelle en Python. typeest lui-même une classe, et c'est son propre type. Vous ne pourrez pas recréer quelque chose comme typepurement en Python, mais Python triche un peu. Pour créer votre propre métaclasse en Python, vous voulez vraiment juste sous-classer type.

Une métaclasse est le plus souvent utilisée comme classe-usine. Lorsque vous créez un objet en appelant la classe, Python crée une nouvelle classe (lorsqu'il exécute l'instruction 'class') en appelant la métaclasse. Combinées avec les méthodes normales __init__et __new__, les métaclasses vous permettent donc de faire des «choses supplémentaires» lors de la création d'une classe, comme enregistrer la nouvelle classe avec un registre ou remplacer la classe par autre chose.

Lorsque l' classinstruction est exécutée, Python exécute d'abord le corps de l' classinstruction comme un bloc de code normal. L'espace de noms résultant (un dict) contient les attributs de la future classe. La métaclasse est déterminée en examinant les classes de base de la classe à venir (les métaclasses sont héritées), l' __metaclass__attribut de la classe à venir (le cas échéant) ou la __metaclass__variable globale. La métaclasse est ensuite appelée avec le nom, les bases et les attributs de la classe pour l'instancier.

Cependant, les métaclasses définissent en fait le type d'une classe, pas seulement une fabrique pour celle-ci, vous pouvez donc faire beaucoup plus avec elles. Vous pouvez, par exemple, définir des méthodes normales sur la métaclasse. Ces méthodes de métaclasse sont comme des méthodes de classe en ce sens qu'elles peuvent être appelées sur la classe sans instance, mais elles ne sont pas non plus comme des méthodes de classe en ce sens qu'elles ne peuvent pas être appelées sur une instance de la classe. type.__subclasses__()est un exemple de méthode sur la typemétaclasse. Vous pouvez également définir les méthodes «magiques» normales, comme __add__, __iter__et __getattr__, pour implémenter ou modifier le comportement de la classe.

Voici un exemple agrégé des morceaux:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType


class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__
Thomas Wouters
la source
13
class A(type):pass<NEWLINE>class B(type,metaclass=A):pass<NEWLINE>b.__class__ = b
pppery
20
ppperry, il voulait évidemment dire que vous ne pouvez pas recréer de type sans utiliser le type lui-même comme métaclasse. Ce qui est assez juste pour le dire.
Holle van
3
Unregister () ne devrait-il pas être appelé par l'instance de la classe Example?
Ciasto piekarz
5
Notez que ce __metaclass__n'est pas pris en charge dans Python 3. Dans l'utilisation de Python 3 class MyObject(metaclass=MyType), voir python.org/dev/peps/pep-3115 et la réponse ci-dessous.
BlackShift
2
La documentation décrit comment la métaclasse est choisie . La métaclasse n'est pas héritée autant qu'elle est dérivée. Si vous spécifiez une métaclasse, il doit s'agir d'un sous-type de chaque métaclasse de classe de base; sinon, vous utiliserez une métaclasse de classe de base qui est un sous-type de chaque métaclasse de classe de base. Notez qu'il est possible qu'aucune métaclasse valide ne soit trouvée et que la définition échoue.
chepner
6820

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 aussi en Python:

>>> 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 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éation dynamique de classes

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 encore écrire toute la classe vous-même.

Les classes étant des objets, elles doivent être générées par quelque chose.

Lorsque vous utilisez le classmot clé, Python crée automatiquement cet objet. Mais comme avec la plupart des choses en Python, cela vous donne un moyen de le faire manuellement.

Rappelez-vous la fonction type? La bonne vieille fonction qui vous permet de savoir de quel type est un objet:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

Eh bien, typea une capacité complètement différente, il peut également créer des classes à la volée. typepeut prendre la description d'une classe comme paramètres et renvoyer une classe.

(Je sais, c'est idiot que la même fonction puisse avoir deux utilisations complètement différentes selon les paramètres que vous lui passez. C'est un problème en raison de la compatibilité descendante 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 valeurs des attributs

par exemple:

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

peut être créé manuellement de cette façon:

>>> 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 classe. Ils peuvent être différents, mais il n'y a aucune raison de compliquer les choses.

typeaccepte un dictionnaire pour définir les attributs de la classe. Donc:

>>> class Foo(object):
...       bar = True

Peut être traduit en:

>>> Foo = type('Foo', (), {'bar':True})

Et utilisé comme 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

Finalement, vous voudrez ajouter des méthodes à votre classe. Définissez simplement une fonction avec la signature appropriée et affectez-la 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 l'ajout de 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 allons: en Python, les classes sont des objets, et vous pouvez créer une classe à la volée, dynamiquement.

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 «truc» qui crée des classes.

Vous définissez des classes afin de créer des objets, non?

Mais nous avons appris que les classes Python sont des objets.

Eh bien, les métaclasses sont ce qui crée ces objets. Ce sont les classes des classes, vous pouvez les imaginer de cette façon:

MyClass = MetaClass()
my_object = MyClass()

Vous avez vu que cela typevous permet de faire quelque chose comme ceci:

MyClass = type('MyClass', (), {})

C'est parce que la fonction typeest en fait une métaclasse. typeest la métaclasse utilisée par Python pour créer toutes les classes dans les coulisses.

Vous vous demandez maintenant pourquoi diable est-il écrit en minuscules et non Type?

Eh bien, je suppose que c'est une question de cohérence avec str, la classe qui crée des objets chaînes et intla classe qui crée des objets entiers. typeest juste la classe qui crée des objets de classe.

Vous voyez cela en vérifiant l' __class__attribut.

Tout, et je veux dire tout, est un objet en Python. Cela inclut les entiers, les chaînes, les fonctions et les classes. Ce sont tous des objets. Et tous 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__problème __class__?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

Ainsi, une métaclasse est juste ce qui crée des objets de classe.

Vous pouvez l'appeler une «usine de classe» si vous le souhaitez.

type est la métaclasse intégrée utilisée par Python, mais bien sûr, vous pouvez créer votre propre métaclasse.

L' __metaclass__attribut

En Python 2, vous pouvez ajouter un __metaclass__attribut lorsque vous écrivez une classe (voir la section suivante pour la syntaxe 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 d' class Foo(object)abord, mais l'objet de classe Foon'est pas encore créé en mémoire.

Python recherchera __metaclass__dans la définition de classe. S'il le trouve, il l'utilisera pour créer la classe d'objets Foo. Si ce n'est pas le cas, il utilisera typepour créer la classe.

Lisez cela plusieurs fois.

Quand vous faites:

class Foo(Bar):
    pass

Python fait ce qui suit:

Y a-t-il un __metaclass__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 Fooen utilisant ce qui est dedans __metaclass__.

Si Python ne peut pas trouver __metaclass__, il recherchera un __metaclass__au niveau MODULE, et essaiera de faire la même chose (mais seulement pour les classes qui n'héritent de rien, essentiellement des classes à l'ancienne).

S'il n'en trouve pas __metaclass__du tout, il utilisera la Barpropre métaclasse de (le premier parent) (qui pourrait être la valeur par défaut type) pour créer l'objet de classe.

Attention ici que l' __metaclass__attribut ne sera pas hérité, la métaclasse du parent ( Bar.__class__) le sera. Si Barutilisé un __metaclass__attribut créé Baravec type()(et non type.__new__()), les sous-classes n'hériteront pas de ce comportement.

Maintenant, la grande question est, que pouvez-vous y mettre __metaclass__?

La réponse est: quelque chose qui peut créer une classe.

Et qu'est-ce qui peut créer une classe? typeou tout ce qui le sous-classe ou l'utilise.

Métaclasses dans 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 que l' __metaclass__attribut n'est plus utilisé, en faveur d'un argument mot-clé dans la liste des classes de base.

Le comportement des métaclasses reste cependant sensiblement le même .

Une chose ajoutée aux métaclasses en python 3 est que vous pouvez également passer des attributs en tant qu'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 lors de sa création.

Vous le faites généralement pour les API, où vous souhaitez créer des classes correspondant au contexte actuel.

Imaginez un exemple stupide, où vous décidez que toutes les classes de votre module devraient avoir leurs attributs écrits en majuscules. Il existe plusieurs façons de procéder, mais l'une d'elles consiste à définir __metaclass__au niveau du module.

De cette façon, toutes les classes de ce module seront créées à l'aide de cette métaclasse, et nous n'avons qu'à dire à la métaclasse de transformer tous les attributs en majuscules.

Heureusement, il __metaclass__peut en fait être appelable, il n'a pas besoin d'être une classe formelle (je sais, quelque chose avec «classe» dans son nom n'a pas besoin d'être une classe, allez comprendre ... 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'

Allons vérifier:

>>> 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: __new__reçoit toujours la classe dans laquelle il est défini, comme premier paramètre. Tout comme vous l'avez selfpour 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 la POO appropriée. Nous appelons typedirectement et nous ne remplaçons ni n'appelons les parents __new__. Faisons cela à la place:

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 des métaclasses, héritant du 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 en python 3 si vous faites cet appel avec des arguments de mots clés, comme ceci:

class Foo(object, metaclass=MyMetaclass, kwarg1=value1):
    ...

Cela 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 plus rien sur les métaclasses.

La raison de la complexité du code utilisant des métaclasses n'est pas à cause des métaclasses, c'est parce que vous utilisez généralement des métaclasses pour faire des trucs tordus en s'appuyant sur l'introspection, en manipulant l'héritage, des variables telles que __dict__, etc.

En effet, les métaclasses sont particulièrement utiles pour faire de la magie noire, et donc des trucs compliqués. Mais par eux-mêmes, ils sont simples:

  • intercepter une création de classe
  • modifier la classe
  • retourner la classe modifiée

Pourquoi utiliseriez-vous des classes de métaclasses au lieu de fonctions?

Puisque __metaclass__peut accepter n'importe quel appelable, pourquoi utiliseriez-vous une classe car elle est évidemment plus compliquée?

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 la POO. La métaclasse peut hériter de la métaclasse et remplacer les méthodes parentes. 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 classe de métaclasse, mais pas avec une fonction de métaclasse.
  • Vous pouvez mieux structurer votre code. Vous n'utilisez jamais de 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 créer plusieurs méthodes et de les regrouper en une seule classe est très utile pour faciliter la lecture du code.
  • Vous pouvez vous accrocher __new__, __init__et __call__. Ce qui vous permettra de faire différentes choses. Même si vous pouvez généralement tout faire __new__, certaines personnes sont tout simplement plus à l'aise __init__.
  • Ce sont des métaclasses, bon sang! Ça doit vouloir dire quelque chose!

Pourquoi utiliseriez-vous des métaclasses?

Maintenant, la grande question. Pourquoi utiliseriez-vous une fonction obscure sujette aux erreurs?

Eh bien, en général, vous ne le faites pas:

Les métaclasses sont une magie plus profonde dont 99% des utilisateurs ne devraient jamais s'inquiéter. Si vous vous demandez si vous en avez besoin, vous n'en avez pas (les personnes qui en ont réellement besoin savent avec certitude qu'elles en ont besoin, et n'ont pas besoin d'explication pour savoir pourquoi).

Python Guru Tim Peters

Le cas d'utilisation principal d'une métaclasse est la création d'une API. Un exemple typique de ceci est l'ORM Django. 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 vous faites cela:

person = Person(name='bob', age='35')
print(person.age)

Il ne retournera pas d' IntegerFieldobjet. Il renverra un int, et peut même le prendre directement à partir de la base de données.

Ceci est possible car models.Modeldéfinit __metaclass__et utilise de la magie qui transformera le que Personvous venez de définir avec des instructions simples en un crochet complexe vers un champ de base de données.

Django rend quelque chose de complexe simple en exposant une API simple et en utilisant des métaclasses, en recréant du code à partir 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.

Et bien 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 ce sont tous des instances de classes ou des instances de métaclasses.

Sauf pour type.

typeest en fait sa propre métaclasse. Ce n'est pas quelque chose que vous pourriez reproduire en Python pur, et cela se fait en trichant un peu au niveau de l'implémentation.

Deuxièmement, les métaclasses sont compliquées. Vous ne voudrez peut-être pas les utiliser pour des modifications de classe très simples. Vous pouvez changer de classe en utilisant deux techniques différentes:

99% du temps où vous avez besoin d'une modification de classe, il vaut mieux les utiliser.

Mais 98% du temps, vous n'avez pas du tout besoin de modification de classe.

e-satis
la source
30
Il semble que dans Django models.Modelil n'utilise pas __metaclass__mais plutôt class Model(metaclass=ModelBase):pour référencer une ModelBaseclasse qui fait alors la magie de métaclasse susmentionnée. Super article! Voici la source Django: github.com/django/django/blob/master/django/db/models/…
Max Goodridge
15
<< Attention ici que l' __metaclass__attribut ne sera pas hérité, la métaclasse du parent ( Bar.__class__) le sera. Si Barutilisé un __metaclass__attribut créé Baravec type()(et non type.__new__()), les sous-classes n'hériteront pas de ce comportement. >> - Pourriez-vous / quelqu'un expliquer un peu plus en profondeur ce passage?
petrux
15
@MaxGoodridge C'est la syntaxe Python 3 pour les métaclasses. Voir Modèle de données Python 3.6 VS Modèle de données Python 2.7
TBBle
2
Now you wonder why the heck is it written in lowercase, and not Type?- bien parce qu'il est implémenté en C - c'est la même raison pour laquelle defaultdict est en minuscule tandis que OrderedDict (en python 2) est normal CamelCase
Mr_and_Mrs_D
15
C'est une réponse wiki communautaire (donc ceux qui ont commenté avec des corrections / améliorations pourraient envisager de modifier leurs commentaires dans la réponse, s'ils sont sûrs qu'ils sont corrects).
Brōtsyorfuzthrāx,
403

Remarque, cette réponse est pour Python 2.x comme il a été écrit en 2008, les métaclasses sont légèrement différentes dans 3.x.

Les métaclasses sont la sauce secrète qui fait fonctionner la «classe». La métaclasse par défaut pour un nouvel objet de style est appelée «type».

class type(object)
  |  type(object) -> the object's type
  |  type(name, bases, dict) -> a new type

Les métaclasses prennent 3 arguments. « nom », « bases » et « dict »

Voici où commence le secret. Recherchez d'où proviennent le nom, les bases et le dict dans cet exemple de définition de classe.

class ThisIsTheName(Bases, Are, Here):
    All_the_code_here
    def doesIs(create, a):
        dict

Permet de définir une métaclasse qui montrera comment « classe: » l'appelle.

def test_metaclass(name, bases, dict):
    print 'The Class Name is', name
    print 'The Class Bases are', bases
    print 'The dict has', len(dict), 'elems, the keys are', dict.keys()

    return "yellow"

class TestName(object, None, int, 1):
    __metaclass__ = test_metaclass
    foo = 1
    def baz(self, arr):
        pass

print 'TestName = ', repr(TestName)

# output => 
The Class Name is TestName
The Class Bases are (<type 'object'>, None, <type 'int'>, 1)
The dict has 4 elems, the keys are ['baz', '__module__', 'foo', '__metaclass__']
TestName =  'yellow'

Et maintenant, un exemple qui signifie réellement quelque chose, cela rendra automatiquement les variables de la liste "attributs" définies sur la classe, et définies sur Aucune.

def init_attributes(name, bases, dict):
    if 'attributes' in dict:
        for attr in dict['attributes']:
            dict[attr] = None

    return type(name, bases, dict)

class Initialised(object):
    __metaclass__ = init_attributes
    attributes = ['foo', 'bar', 'baz']

print 'foo =>', Initialised.foo
# output=>
foo => None

Notez que le comportement magique qui Initialisedgagne en ayant la métaclasse init_attributesn'est pas transmis à une sous-classe de Initialised.

Voici un exemple encore plus concret, montrant comment vous pouvez sous-classer «type» pour créer une métaclasse qui exécute une action lors de la création de la classe. C'est assez délicat:

class MetaSingleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(MetaSingleton, cls).__call__(*args, **kw)
        return cls.instance

class Foo(object):
    __metaclass__ = MetaSingleton

a = Foo()
b = Foo()
assert a is b
Jerub
la source
169

D'autres ont expliqué comment fonctionnent les métaclasses et comment elles s'intègrent dans le système de type Python. Voici un exemple de leur utilisation. Dans un cadre de test que j'ai écrit, je voulais garder une trace de l'ordre dans lequel les classes étaient définies, afin de pouvoir les instancier plus tard dans cet ordre. J'ai trouvé plus facile de le faire en utilisant une métaclasse.

class MyMeta(type):

    counter = 0

    def __init__(cls, name, bases, dic):
        type.__init__(cls, name, bases, dic)
        cls._order = MyMeta.counter
        MyMeta.counter += 1

class MyType(object):              # Python 2
    __metaclass__ = MyMeta

class MyType(metaclass=MyMeta):    # Python 3
    pass

Tout ce qui est une sous-classe MyTypeobtient alors un attribut de classe _orderqui enregistre l'ordre dans lequel les classes ont été définies.

gentil
la source
Merci pour l'exemple. Pourquoi avez-vous trouvé cela plus facile que d'hériter de MyBase, dont __init__(self)dit type(self)._order = MyBase.counter; MyBase.counter += 1?
Michael Gundlach
1
Je voulais que les classes elles-mêmes, pas leurs instances, soient numérotées.
kindall
Bon, duh. Merci. Mon code réinitialiserait l'attribut de MyType à chaque instanciation et ne définirait jamais l'attribut si une instance de MyType n'était jamais créée. Oops. (Et une propriété de classe pourrait également fonctionner, mais contrairement à la métaclasse, elle n'offre aucun endroit évident pour stocker le comptoir.)
Michael Gundlach
1
Ceci est un exemple très intéressant, notamment parce que l'on peut vraiment voir pourquoi une métaclasse pourrait être nécessaire avec cela, pour fournir une solution à une difficulté spécifique. OTOH J'ai du mal à être convaincu que n'importe qui aurait vraiment besoin d'instancier des objets dans l'ordre dans lequel leurs classes ont été définies: je suppose que nous n'avons qu'à vous croire sur parole :).
Mike rongeur
159

L'une des utilisations des métaclasses consiste à ajouter automatiquement de nouvelles propriétés et méthodes à une instance.

Par exemple, si vous regardez les modèles Django , leur définition semble un peu déroutante. Il semble que vous ne définissiez que les propriétés de classe:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

Cependant, lors de l'exécution, les objets Person sont remplis de toutes sortes de méthodes utiles. Voir la source pour une métaclasserie incroyable.

Antti Rasinen
la source
6
N'est-ce pas l'utilisation de méta-classes qui ajoute de nouvelles propriétés et méthodes à une classe et non à une instance? Pour autant que je l'ai compris, la méta-classe modifie la classe elle-même et, par conséquent, les instances peuvent être construites différemment par la classe modifiée. Cela pourrait être un peu trompeur pour les personnes qui essaient d'obtenir la nature d'une méta-classe. Avoir des méthodes utiles sur les instances peut être obtenu par inhérence normale. La référence au code Django comme exemple est bonne, cependant.
trixn le
119

Je pense que l'introduction d'ONLamp à la programmation des métaclasses est bien écrite et donne une très bonne introduction au sujet malgré plusieurs années déjà.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html (archivé sur 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. On peut facilement voir qu'en Python, les classes doivent également être des objets de première classe pour activer ce comportement.

Je n'en ai jamais écrit moi-même, mais je pense que l'une des plus belles utilisations des métaclasses peut être vue dans le framework Django . Les classes de modèle utilisent une approche de métaclasse pour permettre un style déclaratif d'écriture de nouveaux modèles ou classes de formulaire. 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 ayez pas besoin est de 99%.

Matthias Kestenholz
la source
109

Que sont les métaclasses? A quoi les utilisez-vous?

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, d'où Classvient-il? C'est une instance d'une métaclasse (également pseudocode):

>>> Metaclass(...)
Class

En vrai code, 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'>

Autrement dit

  • Une classe est à une instance comme une métaclasse est à une classe.

    Lorsque nous instancions un objet, nous obtenons une instance:

    >>> object()                          # instantiation of class
    <object object at 0x7f9069b4e0b0>     # instance

    De même, lorsque nous définissons une classe explicitement avec la métaclasse par défaut type, nous l'instancions:

    >>> type('Object', (object,), {})     # instantiation of metaclass
    <class '__main__.Object'>             # instance
  • Autrement dit, une classe est une instance d'une métaclasse:

    >>> isinstance(object, type)
    True
  • Autrement dit, 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'objet personnalisé, nous pouvons utiliser une définition de classe de métaclasse pour modifier le comportement d'un objet de classe.

À quoi peuvent-ils servir? De la documentation :

Les utilisations potentielles des métaclasses sont illimitées. Certaines idées qui ont été explorées comprennent la journalisation, la vérification d'interface, la délégation automatique, la création automatique de propriétés, les proxys, 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 d'absolue nécessité.

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)

Cela revient à appeler fonctionnellement typeavec les arguments appropriés et à affecter le résultat à une variable de ce nom:

name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)

Notez que certaines choses sont automatiquement ajoutées à __dict__l'espace de noms, c'est-à-dire:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, 
'__module__': '__main__', '__weakref__': <attribute '__weakref__' 
of 'Foo' objects>, '__doc__': 'demo'})

La métaclasse de l'objet que nous avons créé, dans les deux cas, est type.

(Une note latérale sur le contenu de la classe __dict__: __module__est là parce que les classes doivent savoir où elles sont définies, __dict__et __weakref__sont là parce que nous ne définissons pas __slots__- si nous définissons__slots__ nous économiserons un peu d'espace dans les instances, comme nous pouvons les refuser __dict__et les __weakref__exclure. Par exemple:

>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})

... mais je m'égare.)

Nous pouvons étendre typecomme toute autre définition de classe:

Voici la valeur par défaut __repr__des classes:

>>> Foo
<class '__main__.Foo'>

L'une des choses les plus précieuses que nous pouvons faire par défaut en écrivant un objet Python est de lui fournir un bien __repr__. Lorsque nous appelons, help(repr)nous apprenons qu'il existe un bon test pour un __repr__qui nécessite également un test d'égalité - obj == eval(repr(obj)). L'implémentation simple suivante de __repr__et __eq__pour les instances de classe de notre classe de type nous fournit une démonstration qui peut améliorer la valeur par défaut __repr__des 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__)

Alors maintenant, quand nous créons un objet avec cette métaclasse, l' __repr__écho sur la ligne de commande offre 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éfinition pour l'instance de classe, nous avons une plus grande capacité à déboguer notre code. Cependant, une vérification plus approfondie avec eval(repr(Class))est peu probable (car les fonctions seraient plutôt impossibles à évaluer par rapport à celles par défaut __repr__).

Une utilisation attendue: __prepare__un espace de noms

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 noms de la classe. Nous ferions cela avec __prepare__lequel retourne le dict d'espace de noms pour la classe s'il est implémenté 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'utilisation:

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éés:

>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')

Remarque, cet exemple a été adapté de la documentation - la nouvelle énumération de la bibliothèque standard le fait.

Nous avons donc instancié une métaclasse en créant une classe. Nous pouvons également traiter la métaclasse comme nous le ferions pour n'importe quelle autre classe. Il a un ordre de résolution de méthode:

>>> 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 que nous ne trouvions 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>})
Aaron Hall
la source
78

Mise à jour 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) à utiliser comme espace de noms pendant la création de la classe. Vous devez renvoyer une instance de l'espace de noms que vous choisissez. Si vous n'implémentez pas __prepare__un normal dictest utilisé.

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

Une métaclasse nue et sans rien faire aimerait:

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 vouliez qu'un code de validation simple s'exécute sur vos attributs - comme s'il devait toujours être un intou 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 voir, vous devez répéter deux fois le nom de l'attribut. Cela rend les fautes de frappe possibles avec des 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 (ne pas utiliser __prepare__car elle 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)

Un échantillon de:

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

Remarque : Cet exemple est assez simple, il aurait également pu être réalisé avec un décorateur de classe, mais vraisemblablement une métaclasse réelle ferait beaucoup plus.

La classe 'ValidateType' pour 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
Ethan Furman
la source
Wow, c'est une nouvelle fonctionnalité impressionnante dont je ne savais pas qu'elle existait en Python 3. Merci pour l'exemple !!
Rich Lysakowski PhD
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.nameet dans ce cas également self.attr). Cela a été ajouté pour ne pas avoir à plonger dans les métaclasses pour ce cas d'utilisation courant spécifique (voir PEP 487).
Lars
68

Rôle d'une __call__()méthode de métaclasse 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 est possible lorsque vous implémentez la __call__()méthode magique sur la classe.

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

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

La __call__()méthode 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 en créons une instance), nous appelons en fait sa __call__()méthode de métaclasse . À ce stade, la plupart des programmeurs Python sont un peu confus car on leur a dit que lors de la création d'une instance comme celle-ci, instance = SomeClass()vous appelez sa __init__()méthode. Certains de ceux qui ont creusé un peu plus profond savent que avant __init__()il y a __new__(). Eh bien, aujourd'hui, une autre couche de vérité est révélée, avant __new__()la métaclasse » __call__().

Étudions la chaîne d'appels de méthode dans la perspective spécifique de la création d'une instance d'une classe.

Il s'agit d'une métaclasse qui se connecte exactement au moment où une instance est créée et au 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

Ceci est 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, créons 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.

Notez que le code ci-dessus ne fait rien de plus que la journalisation des 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. Étant donné que typec'est Meta_1la classe parent ( typeétant la métaclasse parent par défaut) et compte tenu de la séquence de classement de la sortie ci-dessus, nous avons maintenant un indice sur 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 __call__()méthode de la métaclasse est celle qui est appelée en premier. Il délègue ensuite la création de l'instance à la __new__()méthode de la classe et l'initialisation à celle de l'instance __init__(). C'est aussi celui qui renvoie finalement l'instance.

De ce qui précède, il ressort que la métaclasse 'a __call__()également la possibilité de décider si un appel à Class_1.__new__()ou Class_1.__init__()sera éventuellement effectué. Au cours de son exécution, il 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 lorsque l'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
Michael Ekoka
la source
Ceci est un bon ajout à la "réponse acceptée" précédemment votée. Il fournit des exemples de codeurs intermédiaires à mâcher.
Rich Lysakowski PhD
56

Une métaclasse est une classe qui indique comment (certaines) autres classes doivent être créées.

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 différemment, 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 dans le module dépassent la quantité de code qui a été écrite. C'est ici...

#!/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()
Craig
la source
43

La version tl; dr

La type(obj)fonction vous obtient le type d'un objet.

Le type()d'une classe est sa métaclasse .

Pour utiliser une métaclasse:

class Foo(object):
    __metaclass__ = MyMetaClass

typeest sa propre métaclasse. La classe d'une classe est une métaclasse - le corps d'une classe est les arguments passés à la métaclasse qui est utilisée pour construire la classe.

Ici, vous pouvez lire comment utiliser les métaclasses pour personnaliser la construction des classes.

noɥʇʎԀʎzɐɹƆ
la source
42

typeest en fait une metaclass- une classe qui crée une autre classe. La plupart metaclasssont des sous-classes de type. Le metaclassreçoit la newclasse comme premier argument et donne accès à l'objet classe avec les détails mentionnés ci-dessous:

>>> class MetaClass(type):
...     def __init__(cls, name, bases, attrs):
...         print ('class name: %s' %name )
...         print ('Defining class %s' %cls)
...         print('Bases %s: ' %bases)
...         print('Attributes')
...         for (name, value) in attrs.items():
...             print ('%s :%r' %(name, value))
... 

>>> class NewClass(object, metaclass=MetaClass):
...    get_choch='dairy'
... 
class name: NewClass
Bases <class 'object'>: 
Defining class <class 'NewClass'>
get_choch :'dairy'
__module__ :'builtins'
__qualname__ :'NewClass'

Note:

Notez que la classe n'a été instanciée à aucun moment; le simple fait de créer la classe a déclenché l'exécution du metaclass.

Mushahid Khan
la source
27

Les classes Python sont elles-mêmes des objets - comme par exemple - de leur méta-classe.

La métaclasse par défaut, qui est appliquée lorsque vous déterminez des classes comme:

class foo:
    ...

les méta-classes sont utilisées pour appliquer une règle à un ensemble complet de classes. Par exemple, supposons que vous construisez un ORM pour accéder à une base de données et que vous souhaitiez que les enregistrements de chaque table appartiennent à une classe mappée à cette table (en fonction des champs, des règles métier, etc.), une utilisation possible de la métaclasse est par exemple, la logique du pool de connexions, qui est partagée par toutes les classes d'enregistrement de toutes les tables. Une autre utilisation est la logique pour prendre en charge les clés étrangères, ce qui implique plusieurs classes d'enregistrements.

lorsque vous définissez la métaclasse, vous tapez le type de sous-classe et pouvez remplacer les méthodes magiques suivantes pour insérer votre logique.

class somemeta(type):
    __new__(mcs, name, bases, clsdict):
      """
  mcs: is the base metaclass, in this case type.
  name: name of the new class, as provided by the user.
  bases: tuple of base classes 
  clsdict: a dictionary containing all methods and attributes defined on class

  you must return a class object by invoking the __new__ constructor on the base metaclass. 
 ie: 
    return type.__call__(mcs, name, bases, clsdict).

  in the following case:

  class foo(baseclass):
        __metaclass__ = somemeta

  an_attr = 12

  def bar(self):
      ...

  @classmethod
  def foo(cls):
      ...

      arguments would be : ( somemeta, "foo", (baseclass, baseofbase,..., object), {"an_attr":12, "bar": <function>, "foo": <bound class method>}

      you can modify any of these values before passing on to type
      """
      return type.__call__(mcs, name, bases, clsdict)


    def __init__(self, name, bases, clsdict):
      """ 
      called after type has been created. unlike in standard classes, __init__ method cannot modify the instance (cls) - and should be used for class validaton.
      """
      pass


    def __prepare__():
        """
        returns a dict or something that can be used as a namespace.
        the type will then attach methods and attributes from class definition to it.

        call order :

        somemeta.__new__ ->  type.__new__ -> type.__init__ -> somemeta.__init__ 
        """
        return dict()

    def mymethod(cls):
        """ works like a classmethod, but for class objects. Also, my method will not be visible to instances of cls.
        """
        pass

de toute façon, ces deux sont les crochets les plus couramment utilisés. le métaclassage est puissant, et la liste des utilisations du métaclassage est loin d'être exhaustive et exhaustive.

Xingzhou Liu
la source
21

La fonction type () peut renvoyer le type d'un objet ou créer un nouveau type,

par exemple, nous pouvons créer une classe Hi avec la fonction type () et nous n'avons pas besoin de l'utiliser de cette façon avec la classe Hi (objet):

def func(self, name='mike'):
    print('Hi, %s.' % name)

Hi = type('Hi', (object,), dict(hi=func))
h = Hi()
h.hi()
Hi, mike.

type(Hi)
type

type(h)
__main__.Hi

En plus d'utiliser type () pour créer des classes dynamiquement, vous pouvez contrôler le comportement de création de classe et utiliser la métaclasse.

Selon le modèle d'objet Python, la classe est l'objet, donc la classe doit être une instance d'une autre certaine classe. Par défaut, une classe Python est une instance de la classe type. Autrement dit, le type est la métaclasse de la plupart des classes intégrées et la métaclasse des classes définies par l'utilisateur.

class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

class CustomList(list, metaclass=ListMetaclass):
    pass

lst = CustomList()
lst.add('custom_list_1')
lst.add('custom_list_2')

lst
['custom_list_1', 'custom_list_2']

La magie prendra effet lorsque nous passerons des arguments de mots clés dans la métaclasse, elle indique à l'interpréteur Python de créer la CustomList via ListMetaclass. new (), à ce stade, nous pouvons modifier la définition de classe, par exemple, et ajouter une nouvelle méthode, puis renvoyer la définition révisée.

binbjz
la source
11

En plus des réponses publiées, je peux dire que a metaclassdéfinit le comportement d'une classe. Ainsi, vous pouvez définir explicitement votre métaclasse. Chaque fois que Python obtient un mot-clé, classil commence à rechercher le metaclass. S'il n'est pas trouvé - le type de métaclasse par défaut est utilisé pour créer l'objet de la classe. En utilisant l' __metaclass__attribut, vous pouvez définir metaclassvotre classe:

class MyClass:
   __metaclass__ = type
   # write here other method
   # write here one more method

print(MyClass.__metaclass__)

Cela produira la sortie comme ceci:

class 'type'

Et, bien sûr, vous pouvez créer le vôtre metaclasspour définir le comportement de toute classe créée à l'aide de votre classe.

Pour ce faire, votre metaclassclasse de type par défaut doit être héritée car c'est le principal metaclass:

class MyMetaClass(type):
   __metaclass__ = type
   # you can write here any behaviour you want

class MyTestClass:
   __metaclass__ = MyMetaClass

Obj = MyTestClass()
print(Obj.__metaclass__)
print(MyMetaClass.__metaclass__)

La sortie sera:

class '__main__.MyMetaClass'
class 'type'
Andy
la source
4

En programmation orientée objet, une métaclasse est une classe dont les instances sont des classes. Tout comme une classe ordinaire définit le comportement de certains objets, une métaclasse définit le comportement de certaines classes et de leurs instances Le terme métaclasse signifie simplement quelque chose utilisé pour créer des classes. En d'autres termes, c'est la classe d'une classe. La métaclasse est utilisée pour créer la classe. Ainsi, comme l'objet étant une instance d'une classe, une classe est une instance d'une métaclasse. En python, les classes sont également considérées comme des objets.

Venu Gopal Tewari
la source
Plutôt que de donner des définitions livresques, cela aurait été mieux si vous aviez ajouté quelques exemples. La première ligne de votre réponse semble avoir été copiée à partir de l'entrée Wikipedia de Metaclasses.
vraisemblance
@verisimilitude J'apprends aussi pouvez-vous m'aider à améliorer cette réponse en fournissant quelques exemples pratiques de votre expérience ??
Venu Gopal Tewari
2

Voici un autre exemple de son utilisation:

  • Vous pouvez utiliser le metaclasspour changer la fonction de son instance (la classe).
class MetaMemberControl(type):
    __slots__ = ()

    @classmethod
    def __prepare__(mcs, f_cls_name, f_cls_parents,  # f_cls means: future class
                    meta_args=None, meta_options=None):  # meta_args and meta_options is not necessarily needed, just so you know.
        f_cls_attr = dict()
        if not "do something or if you want to define your cool stuff of dict...":
            return dict(make_your_special_dict=None)
        else:
            return f_cls_attr

    def __new__(mcs, f_cls_name, f_cls_parents, f_cls_attr,
                meta_args=None, meta_options=None):

        original_getattr = f_cls_attr.get('__getattribute__')
        original_setattr = f_cls_attr.get('__setattr__')

        def init_getattr(self, item):
            if not item.startswith('_'):  # you can set break points at here
                alias_name = '_' + item
                if alias_name in f_cls_attr['__slots__']:
                    item = alias_name
            if original_getattr is not None:
                return original_getattr(self, item)
            else:
                return super(eval(f_cls_name), self).__getattribute__(item)

        def init_setattr(self, key, value):
            if not key.startswith('_') and ('_' + key) in f_cls_attr['__slots__']:
                raise AttributeError(f"you can't modify private members:_{key}")
            if original_setattr is not None:
                original_setattr(self, key, value)
            else:
                super(eval(f_cls_name), self).__setattr__(key, value)

        f_cls_attr['__getattribute__'] = init_getattr
        f_cls_attr['__setattr__'] = init_setattr

        cls = super().__new__(mcs, f_cls_name, f_cls_parents, f_cls_attr)
        return cls


class Human(metaclass=MetaMemberControl):
    __slots__ = ('_age', '_name')

    def __init__(self, name, age):
        self._name = name
        self._age = age

    def __getattribute__(self, item):
        """
        is just for IDE recognize.
        """
        return super().__getattribute__(item)

    """ with MetaMemberControl then you don't have to write as following
    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age
    """


def test_demo():
    human = Human('Carson', 27)
    # human.age = 18  # you can't modify private members:_age  <-- this is defined by yourself.
    # human.k = 18  # 'Human' object has no attribute 'k'  <-- system error.
    age1 = human._age  # It's OK, although the IDE will show some warnings. (Access to a protected member _age of a class)

    age2 = human.age  # It's OK! see below:
    """
    if you do not define `__getattribute__` at the class of Human,
    the IDE will show you: Unresolved attribute reference 'age' for class 'Human'
    but it's ok on running since the MetaMemberControl will help you.
    """


if __name__ == '__main__':
    test_demo()

Le metaclassest puissant, il y a beaucoup de choses (comme la magie des singes) que vous pouvez faire avec, mais attention cela ne peut être connu que de vous.

Carson
la source
2

Une classe, en Python, est un objet, et comme tout autre objet, c'est une instance de "quelque chose". Ce "quelque chose" est ce qu'on appelle une métaclasse. Cette métaclasse est un type spécial de classe qui crée des objets d'autres classes. Par conséquent, la métaclasse est responsable de la création de nouvelles classes. Cela permet au programmeur de personnaliser la façon dont les classes sont générées.

Pour créer une métaclasse, le remplacement des méthodes new () et init () est généralement effectué. new () peut être surchargé pour changer la façon dont les objets sont créés, tandis que init () peut être surchargé pour changer la façon d'initialiser l'objet. La métaclasse peut être créée de plusieurs façons. L'une des façons consiste à utiliser la fonction type (). La fonction type (), lorsqu'elle est appelée avec 3 paramètres, crée une métaclasse. Les paramètres sont: -

  1. Nom du cours
  2. Tuple ayant des classes de base héritées par classe
  3. Un dictionnaire ayant toutes les méthodes de classe et les variables de classe

Une autre façon de créer une métaclasse consiste à utiliser le mot clé «métaclasse». Définissez la métaclasse comme une classe simple. Dans les paramètres de la classe héritée, passez metaclass = metaclass_name

La métaclasse peut être spécifiquement utilisée dans les situations suivantes: -

  1. lorsqu'un effet particulier doit être appliqué à toutes les sous-classes
  2. Un changement de classe automatique (à la création) est requis
  3. Par les développeurs d'API
Swati Srivastava
la source
2

Notez que dans python 3.6, une nouvelle méthode dunder a __init_subclass__(cls, **kwargs)été introduite pour remplacer de nombreux cas d'utilisation courants pour les métaclasses. Is est appelée lorsqu'une sous-classe de la classe de définition est créée. Voir les documents python .

Lars
la source
-3

La métaclasse est une sorte de classe qui définit comment la classe se comportera ou on peut dire qu'une classe est elle-même une instance d'une métaclasse.

AD technique
la source