0

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?

Community
  • 1
  • 1
Flexo
  • 87,323
  • 22
  • 191
  • 272
  • 2
    Can't you just make a factory function that instantiates the appropriate class, instead of trying to do this within the class itself? That would be much simpler. – BrenBarn Apr 29 '14 at 20:34
  • @BrenBarn not really as that would impose more on people who write the `C` objects and that would still require writing two versions of `C` as far as I can make out. – Flexo Apr 29 '14 at 20:36
  • 1
    What you're trying to do doesn't add up, because you're trying the change *class's mro* upon creation of an *instance*... What do you expect to happen if later another instance is created with a different set of arguments? In other words, this is probably an XY problem. – shx2 Apr 29 '14 at 20:39
  • @shx2 what I wanted to happen was effectively create multiple classes by cloning and then modifying the class during creation, on demand. In effect once it works I'd expect there to be multiple `__mro__` in multiple types, transparently to the user. – Flexo Apr 29 '14 at 20:41
  • 1
    In that case, @BrenBarn's factory suggestion is the way to go IMO. You can't have one class with two different mro's, just like you can't have one instance with two different attributes named `xyz`. You need two class objects for that. – shx2 Apr 29 '14 at 20:42
  • Can you clarify a bit more specifically the behavior you want? As it stands, I don't see how your requirements can be met. The metaclass controls creation of the *class*, not the instance, so at the time the metaclass is active, no instances of C exist yet, so you can't know what arguments are going to be passed to instantiate it. – BrenBarn Apr 29 '14 at 21:05
  • You could try to add your magic directly in `B.__new__`, but ultimately I think it's not promising. If the users are writing `C`, they can do anything they want there, so there's no way you can reliably interfere with what happens when `C` is instantiated. It is lower in the inheritance chain, so its implementations will always be called *before* any magic you try to add in its bases. – BrenBarn Apr 29 '14 at 21:05
  • @BrenBarn I want to write `class C(B): ...` but have it such that writing `C('a','b','c')` picks the base class (`AimplN`) based on the values passed in at construction time. I assume that means in reality that there will be N types that get dynamically created. Meta classes could be a big redherring here, I thought they were the way to hook into the instance creation to get the access I needed to achieve it. – Flexo Apr 29 '14 at 21:11
  • 1
    Is that all you want, or do you want to set up a system that allows other people to write such classes `C` and add whatever behavior they want to them? There's a big difference between writing a class that does that and writing a base class that will allow (or force) any inheriting class to do that. – BrenBarn Apr 29 '14 at 21:14
  • @BrenBarn I think that's the key question here... – Jon Clements Apr 29 '14 at 21:16
  • @BrenBarn the goal is to get the simple writing of classes `C`, but it really does need to have the correct base class transparently picked, because they (the `AimplN`) are C++ wrapped instantiations of template specialisations. (The inheritance and distinctness of types are both important) – Flexo Apr 29 '14 at 21:21
  • @Flexo so *mixing in* a user-written `C` with a dynamically derived type `A` then - would it be fair to describe it as that? eg: Someone writes a C, and their class should be created with a suitable `A` as a base behind the scenes... C->A works as normal per classes, but `A` is *explicitly chosen* by the writer of `C`... – Jon Clements Apr 29 '14 at 21:27
  • Kind of - `A` is implicitly chosen by the writer of `C` from the types the pass to `B` at creation type. – Flexo Apr 29 '14 at 21:34
  • @Flexo I meant to write *not explicitly chosen*... *implicit* would certainly have been easier :) – Jon Clements Apr 29 '14 at 21:37
  • Do you need for the specializations of `C` that share the same `AImpX` base have the same class? That is, is it necessary for `type(C(*args)) == type(C(*args))` for all valid `args`? – Blckknght Apr 29 '14 at 22:31

4 Answers4

3

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.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • This looks exactly like the solution I'd envisaged. The is instance checking point is useful although nobody should really be looking too closely at that anyway. – Flexo Apr 30 '14 at 06:33
2

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)
Jon Clements
  • 138,671
  • 33
  • 247
  • 280
  • This is more or less what I suggested. The problem with this is that it doesn't allow a user to actually add behavior to C, which is what the OP seems to want. – BrenBarn Apr 29 '14 at 21:13
  • @BrenBarn good point... thought it might be worth getting some form of answer out so that it's easier for the OP to comment on whether the functionality is correct or not... These kinds of questions are difficult enough in theirselves without having a base of some sort to compare against... – Jon Clements Apr 29 '14 at 21:17
  • @BrenBarn just remembered to click the CW box so it can serve further to that if needs be... – Jon Clements Apr 29 '14 at 21:20
  • That's essentially the behaviour I was hoping for, but not in the form I was looking for because a) it exposes `type` to people writing `C` and b) `C` doesn't implement the abstract methods in the `AimplN`s. – Flexo Apr 29 '14 at 21:26
  • @JonClements that's a similar, but still not ideal workaround - it still doesn't hide the logic about selection of the correct `AimplN` really (which is just an implementation detail of the underlying library) and it still doesn't hide the complexity of multiple types from people implementing C – Flexo Apr 29 '14 at 21:33
2

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.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • That's looking good, 2. Is exactly what I was hoping for, I might have a fiddle with this to see if I can make the "secret" attribute work neatly. – Flexo Apr 29 '14 at 21:45
  • I've suggested a class decorator, but at this stage it's really not much different except avoiding a `classmethod` and different syntax (decoration instead of inheritance from `B`) @Flexo – Jon Clements Apr 29 '14 at 22:34
1

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()
kitti
  • 14,663
  • 31
  • 49