2011-01-10 13 views
21

¿Es posible encadenar metaclases?¿Mixina o encadenamiento de la clase?

Tengo clase Model que usa __metaclass__=ModelBase para procesar su espacio de nombres dict. Voy a heredar de él y "atar" otra metaclase para que no sombree la original.

primer enfoque es subclase class MyModelBase(ModelBase):

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

Pero es posible sólo para ellos como cadena mixins, sin subclases explícito? Algo así como

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

... o aún mejor: crear un MixIn que utilizará __metaclass__ de la matriz directa de la clase que lo utiliza:

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

La razón: Para una mayor flexibilidad al extender las aplicaciones existentes, quiero crear un mecanismo global para engancharme en el proceso de Model, Form, ... definiciones en Django, por lo que se puede cambiar en tiempo de ejecución.

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


Con su ayuda finalmente lograron llegar a una solución: metaclase MetaProxy.

La idea es: crear una metaclase que invoca una devolución de llamada para modificar el espacio de nombres de la clase se crea, entonces, 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 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!' 
+0

En Python 3.4, esto no parece afirmarse correctamente; falla la línea 113 ('MyModel' no tiene el atributo' washere') – Joost

Respuesta

2

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

Pero puede hacer nuevas metaclases durante el tiempo de ejecución y usarlas. Pero eso es un truco horrible. :)

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

(Sin embargo, evitar hacer este tipo de cosas a menos que usted tiene que, o piensa que es divertido.)

+0

'zope.interface' me dio algunas ideas, ¡gracias! :) – kolypto

+0

Oooh, ahora Django también tendrá ataques de metaclassos. Django * es * el nuevo ZOpe. ;-) –

10

Un tipo sólo puede tener una metaclase, debido a una metaclase se limita a establecer lo que hace la declaración de clase - que tiene más de uno no tendría sentido. Por la misma razón, "encadenar" no tiene sentido: la primera metaclase crea el tipo, entonces, ¿qué se supone que debe hacer la 2ª?

Tendrá que fusionar las dos metaclases (como con cualquier otra clase). Pero eso puede ser complicado, especialmente si no sabes realmente 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 se puede ver se trata implica algunas conjeturas ya, ya que no se sabe muy bien lo que hacen los demás metaclases.Si ambas metaclases son realmente simples, podría funcionar, pero no confiaría demasiado en una solución como esta.

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

+0

Todo es genial, excepto por una dependencia explícita en 'MyModelBase' :) – kolypto

+8

" Por la misma razón, "encadenar" no tiene sentido "- ¿Por qué es eso? Encadenar metaclases debe hacerse a través de la herencia. De hecho, dado que su metaclase amplía el 'tipo de metaclass 'incorporado, ya los está encadenando. Cuando llamas a la base de tu metaclase a través de 'super()', puedes lograr fácilmente metaclases encadenadas por herencia múltiple. –

+0

"Un tipo puede tener solo una metaclase, porque una metaclase simplemente establece lo que hace la declaración de clase" - Más apropiado, diría: La metaclase es del tipo del tipo. Entonces, en el código anterior, el tipo de MyModel es ChainedMeta (try type (MyModel)) o en otras palabras, MyModel es una instancia de ChainedMeta. Claramente, cualquier clase, como cualquier objeto, solo puede tener un tipo, por lo que solo puede tener una metaclase. Solo en aras de la claridad – nadapez

4

No sé de ninguna manera "mezclar" metaclases, pero se puede heredar y anular ellos al igual tu tendrías clases normales

Di Tengo un BaseModel:

class BaseModel(object): 
    __metaclass__ = Blah 

y ahora desea heredar esto en una nueva clase llamada MyModel, pero que desea insertar alguna funcionalidad adicional en la metaclase, pero por lo demás dejar el funcionalidad original intacta. 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 
Cuestiones relacionadas