Un-ABC

Monday 30 December 2013This is 11 years old. Be careful.

Python is well-known for its duck-typing: objects are examined for what they can do rather than for what type they are. But if you like being strict about the methods derived classes have to implement, you can use the abstract base classes in the abc module.

They let you define a class, with some methods defined as abstract, and if those methods aren’t defined in a subclass, the subclass can’t be instantiated:

# abstract.py
from abc import ABCMeta, abstractmethod

class Abstract(metaclass=ABCMeta):

    def concrete(self):
        print("I am concrete")

    @abstractmethod
    def not_defined_yet(self):
        raise NotImplementedError

a = Abstract()

produces:

Traceback (most recent call last):
  File "abstract.py", line 13, in <module>
    a = Abstract()
TypeError: Can't instantiate abstract class Abstract with abstract methods not_defined_yet

This is great when you want to be strict, and can remind you of your pleasant days writing Java! But like Java, you can find yourself in situations where you have an abstract base class with a handful of abstract methods, and know that you only need a few of them. The usual remedy at this point is to define all the missing methods knowing they’ll never be called. This is the worst of “keeping the compiler happy”: you know what you need, but the type checking insists that you go through the motions.

Here’s another option: a class decorator that erases the list of abstract methods, so that the class can be instantiated:

def unabc(cls):
    cls.__abstractmethods__ = ()
    return cls

Now we can make a subclass of our abstract base class, not define any methods, and still instantiate the class:

@unabc
class ShutUpAbc(Abstract):
    pass

just_do_it = ShutUpAbc()    # yay!

If we want to get fancier, we can! The missing abstract methods aren’t going to be called (we think!) but we can provide stub methods just in case. The stub methods will raise an error with a message naming the method. For extra bells and whistles, the message will be settable in the decorator, and the decorator will be usable with or without a customized message:

def unabc(arg):
    """
    Add stub methods to a class to satisfy abstract base classes.

    Usage::

        @unabc
        class NotAbstract(SomeAbstractClass):
            pass

        @unabc('Fake {}')
        class NotAbstract(SomeAbstractClass):
            pass
    """

    def _unabc(cls, msg=arg):
        def make_stub_method(ab_name):
            def stub_method(self, *args, **kwargs):
                meth_name = cls.__name__ + "." + ab_name
                raise NotImplementedError(msg.format(meth_name))
            return stub_method

        for ab_name in cls.__abstractmethods__:
            setattr(cls, ab_name, make_stub_method(ab_name))

        # No more abstract methods!
        cls.__abstractmethods__ = ()
        return cls

    # Handle the possibility that unabc is called without a custom message.
    if isinstance(arg, type):
        return _unabc(arg, "{} isn't implemented, and won't be!")
    else:
        return _unabc

Here the _unabc function is the actual decorator. It loops over all the abstract method names, and makes a new stub method for each one. The make_stub_method function is needed because we need to close over the ab_name variable so it will have the proper value when called.

Then stub_method is defined as the actual method that will be added to the class with setattr. Yes, this is four defs nested inside each other: one to define the decorator you use, one to be the actual decorator applied to the class, one to form a closure so we can define stub methods, and one to create the stub methods themselves!

The last part here is to deal with the two ways the unabc decorator can be used: if it’s used without an argument, then the class in question will be the argument, and the isinstance check will be true. In that case, we’ll use the argument as the class, and provide a default message. If the argument isn’t a class, then we return _unabc, and the argument is already provided as a default msg for the _unabc function.

BTW: all the code above is Python 3. The only thing to change for Python 2 is how the ABCMeta metaclass is associated with your abstract class:

class Abstract(object):
    __metaclass__ = ABCMeta
    ...

Comments

[gravatar]
I never really understood the desire for ABC, when is it not sufficient to have the abstract methods raise NotImplemented? What is the use in having it exception at instantiation instead of use? is it just another instance of type checking as a crutch for people who don't have adequate test coverage?
[gravatar]
Graham Dumpleton 10:36 AM on 1 Jan 2014


As is always my gripe when people make use of decorator functions, this fails to preserve introspect-ability of the wrapped abstract methods. Sure you can address this with a bit more work, but as a bit of fun I offer my alternative implementation which uses my wrapt module.

import wrapt
import functools

def unabc(wrapped=None, *, msg="{} isn't implemented, and won't be!"):
    if wrapped is None:
        return functools.partial(unabc, msg=msg)

    @wrapt.decorator
    def _wrapper(wrapped, instance, args, kwargs):
        name = instance.__class__.__name__ + "." + wrapped.__name__
        raise NotImplementedError(msg.format(name))

    for name in wrapped.__abstractmethods__:
        setattr(wrapped, name, _wrapper(getattr(wrapped, name)))

    wrapped.__abstractmethods__ = ()
    return wrapped

@unabc(msg="Give up. {} isn't implemented, and won't be!")
class AlsoShutUpAbc(Abstract):
    pass


This preserves introspect-ability for both the function name, signature and ability to get access to the original wrapped function source code.

This example also presents another way of handling optional decorator parameters. Specifically, I have used Python 3 keyword only argument syntax to enforce message argument is named. For Python 2, you could dispense with the Python 3 specific syntax and still do the same thing, but wouldn't be enforced and users would just need to remember they had to name the message argument.

Anyway, something interesting to kick the new year off with. :-)

[gravatar]
You really shouldn't do this - if you only want to implement part of the API, don't inherit from the ABC at all, and do an explicit registration instead.

If you decide to inherit from the ABC, do it right and implement all the methods it asks for. If you don't provide them all then you're breaching the subclass requirements and your code may break when run with an earlier or later version of the the ABC.
[gravatar]
@Graham: thanks for the pointers to other techniques

@Graham & @Nick: I should have mentioned in the blog post: our desire for implementing only part of the interface was to write tests for small slices of the interface. Without unabc, to test method4, we had to provide boilerplate stubs for methods 1-3. @unabc let us focus on just the parts we cared about.

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.