En Python, que sont les métaclasses et à quoi les utilisons-nous?

5576
e-satis 19 sept. 2008 à 10:10

16 réponses

Meilleure réponse

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.

En Python, vous pouvez utiliser des callables arbitraires pour les métaclasses (comme Jerub montre), la meilleure approche consiste à en faire une classe proprement dite. type est la métaclasse habituelle en Python. type est lui-même une classe et c'est son propre type. Vous ne pourrez pas recréer quelque chose comme type uniquement 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 usine de classe. 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 complètement la classe par autre chose.

Lorsque l'instruction class est exécutée, Python exécute d'abord le corps de l'instruction class 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'attribut __metaclass__ de la classe à venir (le cas échéant) ou la variable globale __metaclass__ . 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 qu'elles ne peuvent pas être appelées sur une instance de la classe. type.__subclasses__() est un exemple de méthode sur la métaclasse type. Vous pouvez également définir les méthodes «magiques» normales, telles que __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__
2757
Cameron Savage 4 mars 2019 à 21:34

Voici un autre exemple de son utilisation:

  • Vous pouvez utiliser le metaclass pour 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 metaclass est 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.

1
Carson Arucard 20 déc. 2019 à 11:03

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ù viennent 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 que Initialised acquiert en ayant la métaclasse init_attributes n'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
386
ralh 6 nov. 2019 à 07:57

Rôle d'une méthode __call__() 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 méthode magique __call__() sur la classe.

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

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

La méthode __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 en créons une instance), nous appelons en fait sa métaclasse '{{X1} } méthode. À 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 méthode __init__(). 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__() 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 renvoyer.

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. Puisque type est la classe parente de Meta_1 (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 méthode __call__() de la métaclasse est celle qui est appelée en premier. Il délègue ensuite la création de l'instance à la méthode __new__() de la classe et l'initialisation à la __init__() de l'instance. C'est aussi celui qui renvoie finalement l'instance.

De ce qui précède, il résulte que la métaclasse '__call__() a également la possibilité de décider si un appel à Class_1.__new__() ou Class_1.__init__() sera finalement 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
65
Michael Ekoka 27 août 2018 à 17:21

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é à 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 la suivante: si vous ne savez pas ce que sont les métaclasses, la probabilité que vous n'en ayez pas besoin est de 99%.

113
Yet Another User 13 août 2018 à 04:53

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 ressemble à un un peu déroutant. 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. Reportez-vous à la source pour découvrir des métaclasseries incroyables.

154
Antti Rasinen 19 sept. 2008 à 06:45

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. nouveau (), à 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.

20
binbjz 12 janv. 2018 à 09:30

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.

3
Venu Gopal Tewari 9 juil. 2019 à 05:45

En plus des réponses publiées, je peux dire qu'un metaclass définit le comportement d'une classe. Ainsi, vous pouvez définir explicitement votre métaclasse. Chaque fois que Python obtient un mot clé class, il 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'attribut __metaclass__, vous pouvez définir metaclass de votre 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 votre propre metaclass pour définir le comportement de toute classe créée à l'aide de votre classe.

Pour ce faire, votre classe de type metaclass par défaut doit être héritée car c'est la principale 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'
10
Andy 15 sept. 2018 à 13:17

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 de MyType obtient alors un attribut de classe _order qui enregistre l'ordre dans lequel les classes ont été définies.

159
kindall 28 nov. 2016 à 18:04

La version tl; dr

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

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

Pour utiliser une métaclasse:

class Foo(object):
    __metaclass__ = MyMetaClass

type est 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 découvrir comment utiliser les métaclasses pour personnaliser la construction des classes.

39
noɥʇʎԀʎzɐɹƆ 5 déc. 2019 à 16:27

type est en fait une metaclass - une classe qui crée une autre classe. La plupart des metaclass sont des sous-classes de type. Le metaclass reçoit la classe new comme premier argument et donne accès à l'objet de 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 l'exécution déclenchée par la classe de metaclass.

39
Chankey Pathak 29 août 2017 à 05:23

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, la substitution des nouvelles () et init () est généralement effectuée. nouveau () peut être remplacé pour changer la façon dont les objets sont créés, tandis que init () peut être remplacé 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
1
Swati Srivastava 20 janv. 2020 à 06:59

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 dict normal est 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 souhaitiez exécuter un code de validation simple sur vos attributs, comme s'il devait toujours s'agir d'un int ou d'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 (sans 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 une métaclasse réelle ferait probablement 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
74
Ethan Furman 1 mars 2016 à 19:48

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 sous-classe le type, 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.

25
Xingzhou Liu 13 juil. 2017 à 08:18

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()
53
3 revs, 2 users 99% 25 janv. 2016 à 20:08
100003