Métaclasse Mixin ou chaînage?

Est-il possible d'enchaîner des métaclasses?

J'ai la classe Model qui utilise __metaclass__=ModelBase pour traiter son dict d'espace de noms. Je vais en hériter et "lier" une autre métaclasse afin qu'elle ne nuise pas à l'original.

La première approche consiste à sous-classer class MyModelBase(ModelBase):

MyModel(Model):
    __metaclass__ = MyModelBase # inherits from `ModelBase`

Mais est-il possible de les enchaîner comme des mixins, sans sous-classement explicite? Quelque chose comme

class MyModel(Model):
    __metaclass__ = (MyMixin, super(Model).__metaclass__)

... ou encore mieux: créez un MixIn qui utilisera __metaclass__ à partir du parent direct de la classe qui l'utilise:

class MyModel(Model):
    __metaclass__ = MyMetaMixin, # Automagically uses `Model.__metaclass__`

La raison: Pour plus de flexibilité dans l'extension des applications existantes, je veux créer un mécanisme mondial pour l'accrochage dans le processus de Model, Form, ... définitions dans Django afin qu'il puisse être modifié au moment de l'exécution.

Un mécanisme commun serait beaucoup mieux que d'implémenter plusieurs métaclasses avec des mixins de rappel.


Avec votre aide, j'ai finalement réussi à trouver une solution: metaclass MetaProxy.

L'idée est: créer un métaclasse qui appelle un rappel pour modifier l'espace de noms de la classe en cours de création, puis, à l'aide de __new__, muter en une métaclasse de l'un des parents

#!/usr/bin/env python
#-*- coding: utf-8 -*-

# Magical metaclass
class MetaProxy(type):
    """ Decorate the class being created & preserve __metaclass__ of the parent

        It executes two callbacks: before & after creation of a class, 
        that allows you to decorate them.

        Between two callbacks, it tries to locate any `__metaclass__` 
        in the parents (sorted in MRO). 
        If found — with the help of `__new__` method it
        mutates to the found base metaclass. 
        If not found — it just instantiates the given class.
        """

    @classmethod
    def pre_new(cls, name, bases, attrs):
        """ Decorate a class before creation """
        return (name, bases, attrs)

    @classmethod
    def post_new(cls, newclass):
        """ Decorate a class after creation """
        return newclass

    @classmethod
    def _mrobases(cls, bases):
        """ Expand tuple of base-classes ``bases`` in MRO """
        mrobases = []
        for base in bases:
            if base is not None: # We don't like `None` :)
                mrobases.extend(base.mro())
        return mrobases

    @classmethod
    def _find_parent_metaclass(cls, mrobases):
        """ Find any __metaclass__ callable in ``mrobases`` """
        for base in mrobases:
            if hasattr(base, '__metaclass__'):
                metacls = base.__metaclass__
                if metacls and not issubclass(metacls, cls): # don't call self again
                    return metacls#(name, bases, attrs)
        # Not found: use `type`
        return lambda name,bases,attrs: type.__new__(type, name, bases, attrs)

    def __new__(cls, name, bases, attrs):
        mrobases = cls._mrobases(bases)
        name, bases, attrs = cls.pre_new(name, bases, attrs) # Decorate, pre-creation
        newclass = cls._find_parent_metaclass(mrobases)(name, bases, attrs)
        return cls.post_new(newclass) # Decorate, post-creation



# Testing
if __name__ == '__main__':
    # Original classes. We won't touch them
    class ModelMeta(type):
        def __new__(cls, name, bases, attrs):
            attrs['parentmeta'] = name
            return super(ModelMeta, cls).__new__(cls, name, bases, attrs)

    class Model(object):
        __metaclass__ = ModelMeta
        # Try to subclass me but don't forget about `ModelMeta`

    # Decorator metaclass
    class MyMeta(MetaProxy):
        """ Decorate a class

            Being a subclass of `MetaProxyDecorator`,
                it will call base metaclasses after decorating
            """
        @classmethod
        def pre_new(cls, name, bases, attrs):
            """ Set `washere` to classname """
            attrs['washere'] = name
            return super(MyMeta, cls).pre_new(name, bases, attrs)

        @classmethod
        def post_new(cls, newclass):
            """ Append '!' to `.washere` """
            newclass.washere += '!'
            return super(MyMeta, cls).post_new(newclass)

    # Here goes the inheritance...
    class MyModel(Model):
        __metaclass__ = MyMeta
        a=1
    class MyNewModel(MyModel):
        __metaclass__ = MyMeta # Still have to declare it: __metaclass__ do not inherit
        a=2
    class MyNewNewModel(MyNewModel):
        # Will use the original ModelMeta
        a=3

    class A(object):
        __metaclass__ = MyMeta # No __metaclass__ in parents: just instantiate
        a=4
    class B(A): 
        pass # MyMeta is not called until specified explicitly



    # Make sure we did everything right
    assert MyModel.a == 1
    assert MyNewModel.a == 2
    assert MyNewNewModel.a == 3
    assert A.a == 4

    # Make sure callback() worked
    assert hasattr(MyModel, 'washere')
    assert hasattr(MyNewModel, 'washere')
    assert hasattr(MyNewNewModel, 'washere') # inherited
    assert hasattr(A, 'washere')

    assert MyModel.washere == 'MyModel!'
    assert MyNewModel.washere == 'MyNewModel!'
    assert MyNewNewModel.washere == 'MyNewModel!' # inherited, so unchanged
    assert A.washere == 'A!'
24
demandé sur kolypto 2011-01-11 00:32:34

3 réponses

Je ne pense pas que vous pouvez les enchaîner comme ça, et je ne sais pas comment cela fonctionnerait non plus.

Mais vous pouvez créer de nouvelles métaclasses pendant l'exécution et les utiliser. Mais c'est un hack horrible. :)

Zope.interface fait quelque chose de similaire, il a une métaclasse advisor, qui va juste faire certaines choses à la classe après la construction. S'il y avait déjà une metclass, l'une des choses qu'il fera définira cette métaclasse précédente comme métaclasse une fois terminée.

(cependant, évitez faire ce genre de choses à moins que vous devez, ou pensez que c'est amusant.)

3
répondu Lennart Regebro 2011-01-10 22:16:52

Un type ne peut avoir qu'une seule métaclasse, car une métaclasse indique simplement ce que fait l'instruction de classe - en avoir plus d'une n'aurait aucun sens. Pour la même raison, le "chaînage"n'a aucun sens: la première métaclasse crée le type, alors que doit faire la 2ème?

Vous devrez fusionner les deux métaclasses (comme avec n'importe quelle autre classe). Mais cela peut être difficile, surtout si vous ne savez pas vraiment ce qu'ils font.

class MyModelBase(type):
    def __new__(cls, name, bases, attr):
        attr['MyModelBase'] = 'was here'
        return type.__new__(cls,name, bases, attr)

class MyMixin(type):
    def __new__(cls, name, bases, attr):
        attr['MyMixin'] = 'was here'
        return type.__new__(cls, name, bases, attr)

class ChainedMeta(MyModelBase, MyMixin):
    def __init__(cls, name, bases, attr):
        # call both parents
        MyModelBase.__init__(cls,name, bases, attr)
        MyMixin.__init__(cls,name, bases, attr)

    def __new__(cls, name, bases, attr):
        # so, how is the new type supposed to look?
        # maybe create the first
        t1 = MyModelBase.__new__(cls, name, bases, attr)
        # and pass it's data on to the next?
        name = t1.__name__
        bases = tuple(t1.mro())
        attr = t1.__dict__.copy()
        t2 = MyMixin.__new__(cls, name, bases, attr)
        return t2

class Model(object):
    __metaclass__ = MyModelBase # inherits from `ModelBase`

class MyModel(Model):
    __metaclass__ = ChainedMeta

print MyModel.MyModelBase
print MyModel.MyMixin

Comme vous pouvez le voir, cela implique des conjectures déjà, puisque vous ne savez pas vraiment ce que font les autres métaclasses. Si les deux métaclasses sont vraiment simples, pourrait fonctionner, mais je n'aurais pas trop confiance en une solution comme celle-ci.

L'écriture d'une métaclasse pour des métaclasses qui fusionnent plusieurs bases est laissée comme un exercice au lecteur ; - P

12
répondu Jochen Ritzel 2011-01-10 22:46:46

Je ne connais aucun moyen de "mélanger" les métaclasses, mais vous pouvez hériter et les remplacer comme vous le feriez pour les classes normales.

Disons que j'ai un BaseModel:

class BaseModel(object):
    __metaclass__ = Blah

Et vous voulez maintenant hériter de cela dans une nouvelle classe appelée MyModel, mais vous voulez insérer des fonctionnalités supplémentaires dans la métaclasse, mais sinon laissez la fonctionnalité d'origine intacte. Pour ce faire, vous feriez quelque chose comme:

class MyModelMetaClass(BaseModel.__metaclass__):
    def __init__(cls, *args, **kwargs):
        do_custom_stuff()
        super(MyModelMetaClass, cls).__init__(*args, **kwargs)
        do_more_custom_stuff()

class MyModel(BaseModel):
    __metaclass__ = MyModelMetaClass
5
répondu Cerin 2013-02-05 22:54:24