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.