Метакласс миксин или цепочка?
Можно ли связать метаклассы?
У меня есть класс Model
, который использует __metaclass__=ModelBase
для обработки своего пространства имен dict. Я собираюсь унаследовать его и "привязать" другой метакласс, чтобы он не затенял исходный.
Первый подход заключается в подклассе class MyModelBase(ModelBase)
:
MyModel(Model):
__metaclass__ = MyModelBase # inherits from `ModelBase`
Но возможно ли просто связать их, как mixins, без явного подкласса? Что-то вроде
class MyModel(Model):
__metaclass__ = (MyMixin, super(Model).__metaclass__)
... или даже лучше: создайте MixIn, который будет использовать __metaclass__
из прямого родителя класса, который его использует:
class MyModel(Model):
__metaclass__ = MyMetaMixin, # Automagically uses `Model.__metaclass__`
Причина. Для большей гибкости в распространении существующих приложений я хочу создать глобальный механизм для подключения к Model
, Form
,... определениям в Django, чтобы его можно было изменить во время выполнения.
Общий механизм был бы намного лучше, чем реализация нескольких метаклассов с миксами обратного вызова.
С вашей помощью мне наконец удалось подойти к решению: metaclass MetaProxy
.
Идея: создать метакласс, который вызывает обратный вызов для изменения пространства имен создаваемого класса, а затем с помощью __new__
вставить в метакласс одного из родителей
#!/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!'
Ответы
Ответ 1
Я не думаю, что вы можете связать их так, и я не знаю, как это будет работать.
Но вы можете создавать новые метаклассы во время выполнения и использовать их. Но это ужасный взлом.:)
zope.interface делает что-то похожее, у него есть метаанализ советника, который будет делать некоторые вещи в классе после построения. Если уже был metclass, одна из вещей, которые он сделает, установит предыдущий метакласс как метакласс после его завершения.
(Тем не менее, избегайте делать такие вещи, если вам не нужно, или думать, что это весело.)
Ответ 2
Тип может иметь только один метакласс, потому что метакласс просто указывает, что делает оператор класса - более чем один из них не имеет смысла. По той же причине "цепочка" не имеет никакого смысла: первый метакласс создает тип, так что же 2-й должен делать?
Вам нужно будет объединить два метакласса (как и с любым другим классом). Но это может быть сложно, особенно если вы действительно не знаете, что они делают.
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 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
Как вы можете видеть, это уже предполагает некоторые догадки, так как вы действительно не знаете, что делают другие метаклассы. Если оба метакласса действительно просты, это может сработать, но я не был бы слишком уверен в таком решении.
Написание метакласса для метаклассов, объединяющих несколько оснований, остается в качестве упражнения для читателя; -P
Ответ 3
Я не знаю, как "смешивать" метаклассы, но вы можете наследовать и переопределять их так же, как обычные классы.
Скажем, у меня есть BaseModel:
class BaseModel(object):
__metaclass__ = Blah
и теперь вы хотите наследовать это в новом классе MyModel, но хотите добавить некоторые дополнительные функции в метакласс, но в противном случае оставить исходную функциональность неповрежденной. Для этого вы бы сделали что-то вроде:
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