4

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?

todofixthis
  • 1,072
  • 1
  • 12
  • 25
  • Despite using `ABCMeta`, your supposedly "abstract" classes in the Python 2 code you show are not actually abstract (that is, you can create instances of them if you want, and Python won't raise an exception). For something to really be abstract you need to use the `@abstractmethod` decorator on some methods you declare within it. Child classes will also be abstract unless they override those methods (without using the decorator themselves). I'm not sure a sane answer can be given to your question without fixing this fundamental issue first. – Blckknght Aug 10 '16 at 22:35
  • I appreciate the input. Your comment prompted me to do some further research, and I see now that my initial understanding of "abstract" was not correct. I'll have to give this some thought; we use this pattern a lot in our codebase, but if we're ultimately using a tool in the wrong way, that's going to cause major problems in the long run. – todofixthis Aug 10 '16 at 22:45

2 Answers2

3

PEP3119 describes how the ABCMeta metaclass "marks" abstract methods and creates an __abstractmethods__ frozenset that contains all methods of a class that are still abstract. So, to check if a class cls is abstract, check if cls.__abstractmethods__ is empty or not.

I also found this relevant post on abstract classes useful.

Community
  • 1
  • 1
George Boukeas
  • 333
  • 1
  • 11
1

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
todofixthis
  • 1,072
  • 1
  • 12
  • 25