¿Metaclass Mixin o encadenamiento?

¿Es posible encadenar metaclases?

Tengo un Model clase que usa __metaclass__=ModelBase para procesar su __metaclass__=ModelBase de espacio de nombres. Voy a heredar de él y “enlazar” otra metaclase para que no oculte el original.

El primer acercamiento es a la class MyModelBase(ModelBase) subclase class MyModelBase(ModelBase) :

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

¿Pero es posible simplemente encadenarlos como mixins, sin subclasificación explícita? Algo como

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

… o incluso mejor: cree un MixIn que usará __metaclass__ del padre directo de la clase que lo usa:

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

La razón: para obtener más flexibilidad en la extensión de las aplicaciones existentes, quiero crear un mecanismo global para conectar el proceso de definiciones de Model , Form , … en Django para que se pueda cambiar en tiempo de ejecución.

Un mecanismo común sería mucho mejor que implementar múltiples metaclases con mixins de callback.


Con su ayuda, finalmente logré encontrar una solución: MetaProxy .

La idea es: crear una metaclase que invoque una callback para modificar el espacio de nombres de la clase que se está creando, y luego, con la ayuda de __new__ , mutar en una metaclase de uno de los padres

 #!/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 Aa == 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!' 

No creo que puedas encadenarlos así, y tampoco sé cómo funcionaría eso.

Pero puedes hacer nuevas metaclases durante el tiempo de ejecución y usarlas. Pero eso es un truco horrible. 🙂

zope.interface hace algo similar, tiene una metaclase asesora, que solo hará algunas cosas a la clase después de la construcción. Si ya existía una metclase, una de las cosas que hará será establecer esa metaclase anterior como la metaclase una vez que haya terminado.

(Sin embargo, evite hacer este tipo de cosas a menos que tenga que hacerlo o piense que es divertido).

Un tipo puede tener solo una metaclase, porque una metaclase simplemente establece lo que hace la statement de clase: tener más de uno no tendría sentido. Por la misma razón, el “encadenamiento” no tiene sentido: la primera metaclase crea el tipo, entonces, ¿qué se supone que debe hacer la segunda?

Tendrá que fusionar las dos metaclases (al igual que con cualquier otra clase). Pero eso puede ser complicado, especialmente si realmente no sabes lo que hacen.

 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 

Como puede ver, esto ya implica algunas conjeturas, ya que realmente no sabe lo que hacen las otras metaclases. Si ambas metaclases son realmente simples, esto podría funcionar, pero no tendría demasiada confianza en una solución como esta.

Escribir una metaclase para las metaclases que fusiona múltiples bases se deja como un ejercicio para el lector;

No conozco ninguna forma de “mezclar” las metaclases, pero puede heredarlas y anularlas como lo haría con las clases normales.

Digamos que tengo un modelo base:

 class BaseModel(object): __metaclass__ = Blah 

y ahora desea heredar esto en una nueva clase llamada MyModel, pero desea insertar alguna funcionalidad adicional en la metaclase, pero de lo contrario, deje intacta la funcionalidad original. Para hacer eso, harías algo como:

 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