Martin McBride, 2021-08-28
Tags callable object closure
Categories magic methods
A callable object is an object that can be used like a function. That is, it can be invoked, ie "called", using brackets (). It can also be passed in wherever a callable object is required, for example in a map function.
Normal Python functions are one example of callable objects. You can also make your own callable object by implementing the
Example LinePrinter class
Here is a simple class that performs a similar job to
class LinePrinter: def __init__(self): self.line = 0 def print(self, x): print(self.line, x) self.line += 1 printn = LinePrinter() printn.print("a") printn.print("b")
This creates the following output:
0 a 1 b
Making the object callable
This works fine, but it might be even better if instead of writing
printn.print() we could just use
printn(). We can do this by defining a method
class LinePrinter: def __init__(self): self.line = 0 def __call__(self, x): print(self.line, x) self.line += 1 printn = LinePrinter() printn("a") printn("b")
Apart from being neater, this approach has the advantage that we can also use the object
printn as if it were a function object. For example, with
list(map(printn, [10, 20, 30]))
map is a lazy function, it will not execute
printn until it needs to. By creating a
list of the
map we force it to evaluate, and therefore to print the output. The result is:
0 10 1 20 2 30
Changing the signature
You can give the
__call__ method any signature you wish. The object will behave like a function of the required signature (excluding the hidden
class LinePrinter: def __init__(self): self.line = 0 def __call__(self, *args, **kwargs): print(self.line, *args, **kwargs) self.line += 1 printn = LinePrinter() printn("a", "b", "c") printn(10, 20, 30, sep='-')
In this case we have allowed the function to accept
**kwargs, that is a variable number of unnamed and named parameters. These are passed on to the
sep (one of the standard
0 a b c 1-10-20-30
Notice that the
sep parameter is passed to
- rather than space.
For the sake of simplicity, this example is a little contrived. There are other ways to solve the same problem. In this case a closure would provide the same functionality, but in a way that is arguably a little simpler. Here is the code:
def LinePrinter(): line = 0 def fn(*args, **kwargs): nonlocal line print(line, *args, **kwargs) line += 1 return fn printn = LinePrinter() printn("a", "b", "c") printn(10, 20, 30, sep='-')
Here, rather than creating a class, we have created a
LinePrinter function (the outer function). That function has a variable
line and an inner function
fn (that corresponds to the
__call__ method). The outer function, when called, returns an object of the inner function.
The fun part about closures is that the inner function object can still access it's own private copy of the
line variable! So each time the function is called it will increment the count.
In reality, for problems such as this you would most likely use a closure rather than creating your own callable object. Firstly because it is simpler and secondly because it is the standard way to solve this type of problem. If you do things in a non-standard way, anyone reading your code will be scratching their head trying to figure out why you didn't just use a closure.
When to use a callable object
It isn't always possible to use a closure instead of a callable object. A particular example is when you need to change the state of the callable object between calls.
For example, suppose we wanted the ability to reset our LinePrinter, so it starts countuing from zero again. We can simple add a reset method to our class:
class LinePrinter: def __init__(self): self.line = 0 def __call__(self, *args, **kwargs): print(self.line, *args, **kwargs) self.line += 1 def reset(self): self.line = 0 printn = LinePrinter() printn("a", "b", "c") printn(10, 20, 30, sep='-') printn.reset() printn("x", "y", "x")
0 a b c 1-10-20-30 0 x y z
Because of the call to the
reset method, the line number restarts at zero on the third line. This would not be easy to do with a closure.