How to implement a Objective-C-like class cluster in Python

There’s an Objective-C design pattern known as class cluster. A class cluster consist of a public abstract base class and private concrete subclasses. The process of instantiating and working with the instances of the concrete classes is completely transparent, i.e. it is as if we were instantiating the base class and working with its instances, except that “behind the scenes” the most suitable concrete classes are used (usually based on the parameters passed to the -init or factory method). The concrete subclasses are thus an implementation detail virtually invisible in the base class’s interface. (It can get complicated if you need to derive from the base class, though. But that’s another story.)

One way to implement this would be to use a custom factory method. But in Objective-C usually the normal -init method is used for class clusters as well. After all being a class cluster is an implementation detail and is no reason to have a factory method in and of itself.

After some tinkering I came up with this way to accomplish the same in Python 3:

The two calls return a SmallThing and a LargeThing, respectively.

Why use __call__ and metaclasses instead of just __new__? We can define __call__ to take any arguments we need without further consequences. If we defined __new__ with some arguments, Python would then want to be able to call __init__ with the same arguments. While this isn’t visible from the example above, the __call__ approach allows for different __init__ method signatures for the concrete classes. Furthermore, we could make Thing into a proper ABC, with implemented but abstract __init__ with yet another signature – something quite likely in a real-world setting. Also note that this approach requires exactly two definitions of __call__ regardless of the number of concrete subclasses.

That said, using the __new__ method or a custom factory method is also possible, but IMHO it’s an inferior approach in the general case, as explained above.

Why define two metaclasses? Because subclasses inherit the metaclass in Python, and “the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases” (that’s what Python will yell at you if you try to supply something else). The ConcreteThingMeta metaclass overrides ThingMeta and reverts to the behaviour of the default metaclass type (i.e. instantiating the class and calling its __init__ method). For some reason, simply setting the metaclass of our concrete classes to type is allowed, even though it clearly is not a subclass of ThingMeta, but it results in infinite recursion. Thus the ConcreteThingMeta metaclass is also necessary.

Happy clustering!