Метакласс Mixin или Цепочка? - PullRequest
28 голосов
/ 11 января 2011

Возможно ли связать метаклассы?

У меня есть класс Model, который использует __metaclass__=ModelBase для обработки его dict пространства имен. Я собираюсь наследовать от него и «связать» другой метакласс, чтобы он не затенял исходный.

Первый подход к подклассу class MyModelBase(ModelBase):

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

Но возможно ли просто связать их в цепочку, как миксины, без явного разделения на подклассы? Что-то вроде

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

... или даже лучше: создайте MixIn, который будет использовать __metaclass__ из прямого родителя класса, который его использует:

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

Причина: для большей гибкости в расширении существующих приложений я хочу создать глобальный механизм для подключения к процессам определений Model, Form, ... в Django, чтобы его можно было изменить во время выполнения.

Общий механизм был бы намного лучше, чем реализация нескольких метаклассов с миксинами обратного вызова.


С вашей помощью мне наконец удалось найти решение: метакласс 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!'

Ответы [ 3 ]

12 голосов
/ 11 января 2011

Тип может иметь только один метакласс, потому что в метаклассе просто указывается, что делает оператор класса - иметь больше одного не имеет смысла. По той же причине «цепочка» не имеет смысла: первый метакласс создает тип, так что же делать второму?

Вам нужно объединить два метакласса (как и в любом другом классе). Но это может быть сложно, особенно если вы действительно не знаете, что они делают.

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

Как вы можете видеть, это уже предполагает некоторые догадки, поскольку вы на самом деле не знаете, что делают другие метаклассы. Если оба метакласса действительно просты, это может сработать, но я бы не был слишком уверен в таком решении.

Написание метакласса для метаклассов, объединяющих несколько баз, читателю оставлено в качестве упражнения; -P

6 голосов
/ 06 февраля 2013

Я не знаю, как "смешивать" метаклассы, но вы можете наследовать и переопределять их так же, как обычные классы.

Скажем, у меня есть 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
4 голосов
/ 11 января 2011

Я не думаю, что вы можете так цеплять их, и я не знаю, как это будет работать.

Но вы можете создавать новые метаклассы во время выполнения и использовать их. Но это ужасный хак. :)

zope.interface делает что-то подобное, у него есть метакласс-консультант, который просто делает некоторые вещи с классом после построения. Если уже был меткласс, одна из вещей, которые он будет делать, устанавливает предыдущий метакласс в качестве метакласса после его завершения.

(Тем не менее, избегайте подобных вещей, если вам не нужно или не думайте, что это весело.)

...