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