Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Metaclass Mixin or Chaining?

Is it possible to chain metaclasses?

I have class Model which uses __metaclass__=ModelBase to process its namespace dict. I'm going to inherit from it and "bind" another metaclass so it won't shade the original one.

First approach is to subclass class MyModelBase(ModelBase):

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

But is it possible just to chain them like mixins, without explicit subclassing? Something like

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

... or even better: create a MixIn that will use __metaclass__ from the direct parent of the class that uses it:

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

The reason: For more flexibility in extending existing apps, I want to create a global mechanism for hooking into the process of Model, Form, ... definitions in Django so it can be changed at runtime.

A common mechanism would be much better than implementing multiple metaclasses with callback mixins.


With your help I finally managed to come up to a solution: metaclass MetaProxy.

The idea is: create a metaclass that invokes a callback to modify the namespace of the class being created, then, with the help of __new__, mutate into a metaclass of one of the parents

#!/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!' 
like image 621
kolypto Avatar asked Jan 10 '11 21:01

kolypto


People also ask

What is the concept of a metaclass?

A metaclass in Python is a class of a class that defines how a class behaves. A class is itself an instance of a metaclass. A class in Python defines how the instance of the class will behave. In order to understand metaclasses well, one needs to have prior experience working with Python classes.

What is the purpose of metaclass?

Custom metaclasses The main purpose of a metaclass is to change the class automatically, when it's created. You usually do this for APIs, where you want to create classes matching the current context.

Is metaclass inherited?

Every object and class in Python is either an instance of a class or an instance of a metaclass. Every class inherits from the built-in basic base class object , and every class is an instance of the metaclass type .

Which method sets the metaclass of class C to M in Python?

In order to set metaclass of a class, we use the __metaclass__ attribute.


1 Answers

A type can have only one metaclass, because a metaclass simply states what the class statement does - having more than one would make no sense. For the same reason "chaining" makes no sense: the first metaclass creates the type, so what is the 2nd supposed to do?

You will have to merge the two metaclasses (just like with any other class). But that can be tricky, especially if you don't really know what they do.

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 

As you can see this is involves some guesswork already, since you don't really know what the other metaclasses do. If both metaclasses are really simple this might work, but I wouldn't have too much confidence in a solution like this.

Writing a metaclass for metaclasses that merges multiple bases is left as an exercise to the reader ;-P

like image 151
Jochen Ritzel Avatar answered Sep 18 '22 14:09

Jochen Ritzel