【问题标题】:Metaclass Mixin or Chaining?元类混合或链接?
【发布时间】:2011-06-06 19:23:46
【问题描述】:

是否可以链接元类?

我有 Model 类,它使用 __metaclass__=ModelBase 来处理其命名空间字典。我将从它继承并“绑定”另一个元类,这样它就不会遮蔽原来的元类。

第一种方法是继承class MyModelBase(ModelBase)

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

但是是否可以像 mixins 一样将它们链接起来,而无需显式子类化?像

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

... 甚至更好:从使用它的类的直接父级创建一个使用 __metaclass__ 的 MixIn:

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

原因:为了更灵活地扩展现有应用程序,我想创建一个全局机制来挂钩到 Django 中的ModelForm、...定义的过程,以便可以在运行时更改它。

一个通用的机制比使用回调混合实现多个元类要好得多。


在您的帮助下,我终于想出了一个解决方案: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!'

【问题讨论】:

  • 在 Python 3.4 中,这似乎没有正确断言;第 113 行失败(MyModel 没有 washere 属性)

标签: python metaclass


【解决方案1】:

我不认为你可以这样链接它们,我也不知道那会如何工作。

但是您可以在运行时创建新的元类并使用它们。但这是一个可怕的黑客攻击。 :)

zope.interface 做了类似的事情,它有一个顾问元类,它只会在构建后对类做一些事情。如果已经有一个metclass,它会做的一件事就是在它完成后将之前的metaclass设置为metaclass。

(但是,除非您必须这样做,或者认为这很有趣,否则请避免做这些事情。)

【讨论】:

  • 哦,现在 Django 也将获得可怕的元类黑客攻击。 Django 新的 ZOpe。 ;-)
【解决方案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'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

【讨论】:

  • 一切都很好,除了对MyModelBase 的显式依赖:)
  • “出于同样的原因,“链接”毫无意义”——为什么?链接元类应该通过继承来完成。事实上,由于您的元类扩展了内置元类type,因此您已经将它们链接起来。当您通过super() 调用元类的基类时,您可以通过多重继承轻松实现链式元类。
  • “一个类型只能有一个元类,因为一个元类只说明类语句的作用”——我会说更合适:元类是类型的类型。所以在上面的代码中,MyModel 的类型是 ChainedMeta (try type(MyModel)) 或者换句话说,MyModel 是 ChainedMeta 的一个实例。显然,任何类,就像任何对象一样,只能有一种类型,所以只能有一个元类。只是为了清楚起见
  • 非常抱歉,我错误地投了反对票(在触摸屏上触摸),后来看到我的 -1 成本并意识到我这样做了,为时已晚,无法恢复。给我一个?,很好的答案。
【解决方案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

【讨论】:

    【解决方案4】:

    添加到@jochenritzel 的答案,以下简化了组合步骤:

    def combine_classes(*args):
        name = "".join(a.__name__ for a in args)
        return type(name, args, {})
    
    
    class ABCSomething(object, metaclass=combine_classes(SomethingMeta, ABCMeta)):
        pass
    

    在这里,type(name, bases, dict) 的工作方式类似于动态的class 语句(请参阅docs)。令人惊讶的是,似乎没有办法在第二步中使用dict 参数来设置metaclass。否则可以将整个过程简化为一个函数调用。

    【讨论】:

      猜你喜欢
      • 2015-01-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多