Designing with ABC callables

There are two easy ways to create callable objects in Python, as follows:

  • Using the def statement to create a function
  • By creating an instance of a class that uses collections.abc.Callable as its base class

We can also assign a lambda form to a variable. A lambda is a small, anonymous function that consists of exactly one expression. We'd rather not emphasize saving lambdas in a variable as it leads to the confusing situation where we have a function-like callable that's not defined with a def statement. The following is a simple callable object that has been created from a class:

import collections.abc
class Power1( collections.abc.Callable ):
    def __call__( self, x, n ):
        p= 1
        for i in range(n):
            p *= x
        return p
pow1= Power1()

There are three parts to the preceding callable object, as follows:

  • We defined the class as a subclass of abc.Callable
  • We defined the __call__() method
  • We created an instance of the class, pow1()

Yes, the algorithm seems inefficient. We'll address that.

Clearly, this is so simple that a full class definition isn't really necessary. In order to show the various optimizations, it's slightly simpler to start with a callable object rather than mutate a function into a callable object.

We can now use the pow1() function just as we'd use any other function. Here's how to use the pow1() function in a Python command line:

>>> pow1( 2, 0 )
1
>>> pow1( 2, 1 )
2
>>> pow1( 2, 2 )
4
>>> pow1( 2, 10 )
1024

We've evaluated the callable object with various kinds of argument values. It's not required to make a callable object a subclass of abc.Callable. However, it does help with debugging.

Consider this flawed definition:

class Power2( collections.abc.Callable ):
    def __call_( self, x, n ):
        p= 1
        for i in range(n):
            p *= x
        return p

The preceding class definition has an error and doesn't meet the definition of the callable abstraction.

Found the error yet? If not, it's at the end of the chapter.

The following is what happens when we try to create an instance of this class:

>>> pow2= Power2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Power2 with abstract methods __call__

It may not be obvious exactly what went wrong, but we have a fighting chance to debug this. If we hadn't subclassed collections.abc.Callable, we'd have a somewhat more mysterious problem to debug.

Here's what the more mysterious problem would look like. We'll skip the actual code for Power3. It's the same as Power2, except it doesn't subclass collections.abc.Callable. It starts class Power3; otherwise, it's identical.

The following is what happens when we try to use Power3 as a class that doesn't meet the expectations of callables and isn't a subclass of the abc.Callable either:

>>> pow3= Power3()
>>> pow3( 2, 5 )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Power3' object is not callable

This error provides less guidance as to why the Power3 class definition is flawed. The Power2 error is much more explicit about the nature of the problem.