Question

I'm trying to create a metaclass in Python that dynamically changes the base class of a type, during creation depending upon the arguments given when creating the instance.

In short I've got a hierarchy, C --> B --> A but what I want to do is dynamically swap A for other implementations of A if certain things are passed to C for construction.

Because C is what the users of this library implement I didn't want to force them into writing anything that a beginner wouldn't understand, so my plan was to make the magic happen inside B, which only exits for the purpose of diverting A to an appropriate implementation.

Based on my understanding of metaclasses and __new__ I've got as far as:

class A(object):
  pass

class Aimpl1(object):
  def foo(self):
    print "FOO"

class Aimpl2(object):
  def foo(self):
    print "BAR"

class AImplChooser(type):
  def __call__(self, *args, **kwargs):
    print "In call"
    return super(AImplChooser,self).__call__(*args,**kwargs)

  def __new__(cls, clsname, bases, dct):
    print "Creating: " + clsname + ", " + ','.join([str(x) for x in bases])
    return super(AImplChooser,cls).__new__(cls, clsname, bases, dct)

class B(A):
  __metaclass__ = AImplChooser
  def __init__(self, arg1, arg, arg3):
    pass

class C(B):
  def __init__(self, arg1, arg2=0, arg3=[]):
    super(C, self).__init__(arg1, arg2, arg3)

c=C('')

print type(c)
print dir(type(c))
print c.__class__.__bases__

c.foo()

My plan was to divert the bases inside B.__new__ based on the arguments to B.__call__, but of course they don't get called in that order at all, so that's not an option.

I thought about dropping __new__ entirely and doing it all inside __call__, but the problem there is that the objects already exist by that point, so it's too late to change the bases.

What am I missing about classes and meta classes? Is there a way to do this?

Was it helpful?

Solution

I believe I've managed to implement the metaclass you were asking for. I'm not certain if this is the best possible design, but it works. Each notional instance of C is actually an instance of a "specialization" of C, which derives from a specialization of B, which derives from a specialized A class (the A classes need not be related in any way). All instances of a given C specialization will have the same type as one another, but different types than instances with a different specialization. Inheritance works the same way, with the specializations defining separate parallel class trees.

Here's my code:

First, we need to define the specializations of the A class. This can be done however you want, but for my testing I used a list comprehension to build a bunch of classes with different names and different values in a num class variable.

As = [type('A_{}'.format(i), (object,), {"num":i}) for i in range(10)]

Next, we have the "dummy" unspecialized A class, which is really just a place for a metaclass to hook into. A's metaclass AMeta does the lookup in of the specialized A classes in the list I defined defined above. If you use a different method to define the specialized A classes, change AMeta._get_specialization to be able to find them. It might even be possible for new specializations of A to be created on demand here, if you wanted.

class AMeta(type):
    def _get_specialization(cls, selector):
        return As[selector]

class A(object, metaclass=AMeta): # I'm using Python 3 metaclass declarations
    pass # nothing defined in A is ever used, it is a pure dummy

Now, we come to class B and its metaclass BMeta. This is where the actual specialization of our sub-classes takes place. The __call__ method of the metaclass uses the _get_specialization method to build a specialized version of the class, based on a selector argument. _get_specialization caches its results, so only one class is made per specialization at a given level of the inheritance tree.

You could adjust this a bit if you want (use multiple arguments to compute selector, or whatever), and you might want to pass on the selector to the class constructor, depending on what it actually is. The current implementation of the metaclass only allows for single inheritance (one base class), but it could probably be extended to support multiple inheritance in the unlikely event you need that.

Note that while the B class is empty here, you can give it methods and class variables that will appear in each specialization (as shallow copies).

class BMeta(AMeta):
    def __new__(meta, name, bases, dct):
        cls = super(BMeta, meta).__new__(meta, name, bases, dct)
        cls._specializations = {}
        return cls

    def _get_specialization(cls, selector):
        if selector not in cls._specializations:
            name = "{}_{}".format(cls.__name__, selector)
            bases = (cls.__bases__[0]._get_specialization(selector),)
            dct = cls.__dict__.copy()
            specialization = type(name, bases, dct) # not a BMeta!
            cls._specializations[selector] = specialization
        return cls._specializations[selector]

    def __call__(cls, selector, *args, **kwargs):
        cls = cls._get_specialization(selector)
        return type.__call__(cls, *args, **kwargs) # selector could be passed on here

class B(A, metaclass=BMeta):
    pass

With this setup, your users can define any number of C classes that inherit from B. Behind the scenes, they'll really be defining a whole family of specialization classes that inherit from the various specializations of B and A.

class C(B):
    def print_num(self):
        return self.num

It's important to note that C is not ever really used as a regular class. C is really a factory that creates instances of various related classes, not instances of itself.

>>> C(1)
<__main__.C_1 object at 0x00000000030231D0>
>>> C(2)
<__main__.C_2 object at 0x00000000037101D0>
>>> C(1).print_num()
1
>>> C(2).print_num()
2
>>> type(C(1)) == type(C(2))
False
>>> type(C(1)) == type(C(1))
True
>>> isinstance(C(1), type(B(1)))
True

But, here's a perhaps unobvious behavior:

>>> isinstance(C(1), C)
False

If you want the unspecialized B and C types to pretend to be superclasses of their specializations, you can add the following functions to BMeta:

def __subclasscheck__(cls, subclass):
    return issubclass(subclass, tuple(cls._specializations.values()))

def __instancecheck__(cls, instance):
    return isinstance(instance, tuple(cls._specializations.values()))

These will persuade the the built-in isinstance and issubclass functions to treat the instances returned from B and C as instances of their "factory" class.

OTHER TIPS

Here's the closest thing I can muster at the moment:

class A(object):
    pass

class Aimpl1(object):
    def foo(self):
        print "FOO"

class Aimpl2(object):
    def foo(self):
        print "BAR"

class B(object):
    @classmethod
    def makeIt(cls, whichA):
        if whichA == 1:
            impl = Aimpl1
        elif whichA == 2:
            impl = Aimpl2
        else:
            impl = A
        print "Instantiating", impl, "from", cls
        TmpC = type(b'TmpC', (cls,impl), dict(cls.__dict__))

        return TmpC(whichA)

    def __init__(self, whichA):
        pass

class C(B):
    def __init__(self, whichA):
        super(C, self).__init__(whichA)

It can be used this way:

>>> c = C.makeIt(1)
Instantiating <class '__main__.Aimpl1'> from <class '__main__.C'>
>>> c.__class__.__mro__
(<class '__main__.TmpC'>,
 <class '__main__.C'>,
 <class '__main__.B'>,
 <class '__main__.Aimpl1'>,
 <type 'object'>)
>>> c.foo()
FOO
>>> c = C.makeIt(2)
Instantiating <class '__main__.Aimpl2'> from <class '__main__.C'>
>>> c.__class__.__mro__
(<class '__main__.TmpC'>,
 <class '__main__.C'>,
 <class '__main__.B'>,
 <class '__main__.Aimpl2'>,
 <type 'object'>)
>>> c.foo()
BAR

It differs from your setup in a few ways:

  1. The class C must be instantiated using the makeIt classmethod instead of directly with C(blah). This is to avoid an infinite loop. If __new__ is used in B to handle the delegation, but the magically created new class with the switched bases has to inherit from the original C, then the new class will inherit B.__new__, and trying to create one internally will again engage the magic. This could also probably be circumvented by using __new__ and adding a "secret" attribute to the dynamically created class and checking that to skip the magic.

  2. B does not inherit from A, so that when C inherits from B, it won't inherit from A either; instead it gets to inherit from the correctly swapped-in implementation base.

Update: a possible alternative is using a class decorator to act the current B role:

(This still needs a bit of work though).

class A1(object):
    def foo(self):
        print 'foo'
class A2(object):
    def foo(self):
        print 'bar'

from functools import wraps
def auto_impl_A(cls):
    @wraps(cls)
    def f(val, *args, **kwargs):
        base = {1: A1, 2: A2}.get(val, object)
        return type(cls.__name__, (cls, base,), dict(cls.__dict__))(*args, **kwargs)
    return f

@auto_impl_A
class MyC(object):
    pass

So users decorate their class instead of inheriting, and write C as normal, but its base will be an appropriate A...


Original proprosal: If I'm understanding correctly, it's easier to use a factory function and create a new type with suitable bases from the start...

class A1(object): pass
class A2(object): pass
class ANOther(object): pass

def make_my_C_obj(someval, *args, **kwargs):
    base = {1: A1, 2: A2}.get(someval, ANOther)
    return type('C', (base,), {})(*args, **kwargs)

for i in xrange(3):
    print i, type(make_my_C_obj(i)).mro()

0 [<class '__main__.C'>, <class '__main__.ANOther'>, <type 'object'>]
1 [<class '__main__.C'>, <class '__main__.A1'>, <type 'object'>]
2 [<class '__main__.C'>, <class '__main__.A2'>, <type 'object'>]

That's equivalent to:

class Aimpl1(object):
  def foo(self):
    print "FOO"

class Aimpl2(object):
  def foo(self):
    print "BAR"

def C_factory(base):
  class C(base):
    pass
  return C

for b in (Aimpl1, Aimpl2):
  c=C_factory(b)()
  c.foo()
  print type(c)

You could do this using a wrapper:

class Bwrapper(object):
    def __init__(self, impl):
        self._a = Aimpl2() if impl == 2 else Aimpl1()

    def foo(self):
        return self._a.foo()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top