Strange metaclass behaviour

Ziga Seilnacht ziga.seilnacht at gmail.com
Thu Mar 23 09:30:55 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:

I don't know if this is a bug; but I will try to expain
what is happening; here is an example similar to yours:

>>> class M_A(type):
...     def __new__(meta, name, bases, dict):
...         print 'metaclass:', meta.__name__, 'class:', name
...         return super(M_A, meta).__new__(meta, name, bases, dict)
...
>>> class M_B(M_A):
...     pass
...
>>> class A(object):
...     __metaclass__ = M_A
...
metaclass: M_A class: A
>>> class B(object):
...     __metaclass__ = M_B
...
metaclass: M_B class: B

So far everything is as expected.

>>> class C(A, B):
...     __metaclass__ = M_B
...
metaclass: M_B class: C

If we explicitly declare that our derived class inherits
from the second base, which has a more derived metaclass,
everything is OK.

>>> class D(A, B):
...     pass
...
metaclass: M_A class: D
metaclass: M_B class: D

Now this is where it gets interesting; what happens
is the following:
 - Since D does not have a __metaclass__ attribute,
   its type is determined from its bases.
 - Since A is the first base, its type (M_A) is called;
   unfortunately this is not the way metaclasses are
   supposed to work; the most derived metaclass should
   be selected.
 - M_A's __new__ method calls the __new__ method of the
   next class in MRO; that is, super(M_1, meta).__new__
   is equal to type.__new__.
 - In type.__new__, it is determined that M_A is not
   the best type for D class; it should be actually M_B.
 - Since type.__new__ was called with wrong metaclass
   as the first argument, call the correct metaclass.
 - This calls M_B.__new__, which again calls type.__new__,
   but this time with M_B as the first argument, which
   is correct.

As I said, I don't know if this is a bug or not,
but you can achieve what is expected if you do the
following in your __new__ method (warning, untested code):

>>> from types import ClassType
>>> class AnyMeta(type):
...     """
...     Metaclass that follows type's behaviour in "metaclass
resolution".
...
...     Code is taken from Objects/typeobject.c and translated to
Python.
...     """
...     def __new__(meta, name, bases, dict):
...         winner = meta
...         for cls in bases:
...             candidate = type(cls)
...             if candidate is ClassType:
...                 continue
...             if issubclass(winner, candidate):
...                 continue
...             if issubclass(candidate, winner):
...                 winner = candidate
...                 continue
...             raise TypeError("metaclass conflict: ...")
...         if winner is not meta and winner.__new__ !=
AnyMeta.__new__:
...             return winner.__new__(winner, name, bases, dict)
...         # Do what you actually meant from here on
...         print 'metaclass:', winner.__name__, 'class:', name
...         return super(AnyMeta, winner).__new__(winner, name, bases,
dict)
...
>>> class OtherMeta(AnyMeta):
...     pass
...
>>> class A(object):
...     __metaclass__ = AnyMeta
...
metaclass: AnyMeta class: A
>>> class B(object):
...     __metaclass__ = OtherMeta
...
metaclass: OtherMeta class: B
>>> class C(A, B):
...     pass
...
metaclass: OtherMeta class: C

> Does anyone have a detailed explanation here ?
> Is this problem already known ?
> 
> regards
> chris

I hope that above explanation helps.

Ziga




More information about the Python-list mailing list