python - ¿Metaclass Mixin o encadenamiento?
(3)
¿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 devolución de llamada.
Con su ayuda, finalmente logré encontrar una solución: MetaProxy
.
La idea es: crear una metaclase que invoque una devolución de llamada para modificar el espacio de nombres de la clase que se está creando, 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 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!''
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
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 horrible truco. :)
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 solo puede tener una metaclase, porque una metaclase simplemente establece lo que hace la declaración 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;