Search code examples
pythonmetaclasssix

How to tell if a class is abstract in Python 3?


I wrote a metaclass that automatically registers its classes in a dict at runtime. In order for it to work properly, it must be able to ignore abstract classes.

The code works really well in Python 2, but I've run into a wall trying to make it compatible with Python 3.

Here's what the code looks like currently:

def AutoRegister(registry, base_type=ABCMeta):
    class _metaclass(base_type):
        def __init__(self, what, bases=None, attrs=None):
            super(_metaclass, self).__init__(what, bases, attrs)

            # Do not register abstract classes.
            # Note that we do not use `inspect.isabstract` here, as
            #   that only detects classes with unimplemented abstract
            #   methods - which is a valid approach, but not what we
            #   want here.
            # :see: http://stackoverflow.com/a/14410942/
            metaclass = attrs.get('__metaclass__')
            if not (metaclass and issubclass(metaclass, ABCMeta)):
                registry.register(self)

    return _metaclass

Usage in Python 2 looks like this:

# Abstract classes; these are not registered.
class BaseWidget(object):  __metaclass__ = AutoRegister(widget_registry)
class BaseGizmo(BaseWidget): __metaclass__ = ABCMeta

# Concrete classes; these get registered.
class AlphaWidget(BaseWidget): pass
class BravoGizmo(BaseGizmo): pass

What I can't figure out, though, is how to make this work in Python 3.

How can a metaclass determine if it is initializing an abstract class in Python 3?


Solution

  • I couldn't shake the feeling as I was posting this question that I was dealing with an XY Problem. As it turns out, that's exactly what was going on.

    The real issue here is that the AutoRegister metaclass, as implemented, relies on a flawed understanding of what an abstract class is. Python or not, one of the most important criteria of an abstract class is that it is not instanciable.

    In the example posted in the question, BaseWidget and BaseGizmo are instanciable, so they are not abstract.

    Aren't we just bifurcating rabbits here?

    Well, why was I having so much trouble getting AutoRegister to work in Python 3? Because I was trying to build something whose behavior contradicts the way classes work in Python.

    The fact that inspect.isabstract wasn't returning the result I wanted should have been a major red flag: AutoRegister is a warranty-voider.

    So what's the real solution then?

    First, we have to recognize that BaseWidget and BaseGizmo have no reason to exist. They do not provide enough functionality to be instantiable, nor do they declare abstract methods that describe the functionality that they are missing.

    One could argue that they could be used to "categorize" their sub-classes, but a) that's clearly not what's going on in this case, and b) quack.

    Instead, we could embrace Python's definition of "abstract":

    1. Modify BaseWidget and BaseGizmo so that they define one or more abstract methods.

      • If we can't come up with any abstract methods, then can we remove them entirely?
      • If we can't remove them but also can't make them properly abstract, it might be worthwhile to take a step back and see if there are other ways we might solve this problem.
    2. Modify the definition of AutoRegister so that it uses inspect.isabstract to decide if a class is abstract: see final implementation.

    That's cool and all, but what if I can't change the base classes?

    Or, if you have to maintain backwards compatibility with existing code (as was the case for me), a decorator is probably easier:

    @widget_registry.register
    class AlphaWidget(object):
        pass
    
    @widget_registry.register
    class BravoGizmo(object):
        pass