Strange metaclass behaviour

Michele Simionato michele.simionato at gmail.com
Thu Mar 23 10:22:38 EST 2006


Christian Eder wrote:
> Hi,
>
> I think I have discovered a problem in context of
> metaclasses and multiple inheritance in python 2.4,
> which I could finally reduce to a simple example:
>
> Look at following code:
>
> class M_A (type) :
>
>      def __new__ (meta, name, bases, dict) :
>         print "M.__new__", meta, name, bases
>         return super (M_A, meta).__new__ (meta, name, bases, dict)
>
> class M_B (M_A) : pass
>
> class A (object) : __metaclass__ = M_A
>
> class B (object) : __metaclass__ = M_B
>
> class D (A, B) : pass
>
>
> One would expect that either
> 1) D inherits the metaclass M_A from A
> 2) or python raises the well known "Metatype conflict among bases"
> error

No, there is no conflict in this case: since M_B is a subclass of M_A,
the
metaclass of D is M_B. I don't think this is bug either: the fact is
that
type.__new__ works differently from object.__new__, so that it is
called twice in this case. Not sure if it could be avoided.

Speaking of the metatype conflict, I realized a while ago that it is
possible
to avoid it automatically even at the pure Python level, without
changing
the C code for type.__new__. However, the solution is too complicate.
According
to the Zen of Python "If the implementation is hard to explain, it's a
bad idea",
thus I have never used it.

Still, it is an interesting exercise if you are willing to risk the
melting of your brain,
so here is the code ;)

# noconflict.py
"""Deep, **DEEP** magic to remove metaclass conflicts.

``noconflict`` provides the ``safetype`` metaclass, the mother of
conflict-free
metaclasses. I you do

    from noconflict import safetype as type

on top of your module, all your metaclasses will be conflict safe.
If you override ``__new__`` when you derive from ``safetype``,
you should do it cooperatively."""

import inspect, types, __builtin__
try: set # python version >= 2.4
except NameError: # python version <= 2.3
   from sets import Set as set

def skip_redundant(iterable, skipset=None):
   "Redundant items are repeated items or items in the original
skipset."
   if skipset is None: skipset = set()
   for item in iterable:
       if item not in skipset:
           skipset.add(item)
           yield item

memoized_metaclasses_map = {}

# utility function
def remove_redundant(metaclasses):
   skipset = set([types.ClassType])
   for meta in metaclasses: # determines the metaclasses to be skipped
       skipset.update(inspect.getmro(meta)[1:])
   return tuple(skip_redundant(metaclasses, skipset))

##################################################################
## now the core of the module: two mutually recursive functions ##
##################################################################

def get_noconflict_metaclass(bases, left_metas, right_metas):
    """Not intended to be used outside of this module, unless you know
    what you are doing."""
    # make tuple of needed metaclasses in specified priority order
    metas = left_metas + tuple(map(type, bases)) + right_metas
    needed_metas = remove_redundant(metas)

    # return existing confict-solving meta, if any
    if needed_metas in memoized_metaclasses_map:
      return memoized_metaclasses_map[needed_metas]
    # nope: compute, memoize and return needed conflict-solving meta
    elif not needed_metas:         # wee, a trivial case, happy us
        meta = type
    elif len(needed_metas) == 1: # another trivial case
        meta = needed_metas[0]
    # check for recursion, can happen i.e. for Zope ExtensionClasses
    elif needed_metas == bases:
        raise TypeError("Incompatible root metatypes", needed_metas)
    else: # gotta work ...
        metaname = '_' + ''.join([m.__name__ for m in needed_metas])
        meta = classmaker()(metaname, needed_metas, {})
    memoized_metaclasses_map[needed_metas] = meta
    return meta

def classmaker(left_metas=(), right_metas=()):
    def make_class(name, bases, adict):
        metaclass = get_noconflict_metaclass(bases, left_metas,
right_metas)
        return metaclass(name, bases, adict)
    return make_class

#################################################################
## and now a conflict-safe replacement for 'type'              ##
#################################################################

__type__=__builtin__.type # the aboriginal 'type'
# left available in case you decide to rebind __builtin__.type

class safetype(__type__):
    """Overrides the ``__new__`` method of the ``type`` metaclass,
making the
    generation of classes conflict-proof."""
    def __new__(mcl, *args):
        nargs = len(args)
        if nargs == 1: # works as __builtin__.type
            return __type__(args[0])
        elif nargs == 3: # creates the class using the appropriate
metaclass
            n, b, d = args # name, bases and dictionary
            meta = get_noconflict_metaclass(b, (mcl,), right_metas=())
            if meta is mcl: # meta is trivial, dispatch to the default
__new__
                return super(safetype, mcl).__new__(mcl, n, b, d)
            else: # non-trivial metaclass, dispatch to the right
__new__
                # (it will take a second round)
                return super(mcl, meta).__new__(meta, n, b, d)
        else:
            raise TypeError('%s() takes 1 or 3 arguments' %
mcl.__name__)



              Michele Simionato




More information about the Python-list mailing list