Callable Protocol in Python

Use objects as functions in Python.

Sergio Daniel Cortez Chavez
Python in Plain English

--

Photo by Luke Southern on Unsplash

One of the great strengths of Python is the expressive power it contains through its protocols. Have you ever come across a new class that allows its instances to be called like a function and at the same time have other functionalities that are not standard for “function” objects?

In this post, I will show you how it is possible to use our objects as a function, at the same time as deepening the design of the Python API.

Callable objects

A Callable object is defined as an object that has implemented the __call__ method. By default, any kind of function object (lambda, named function) implement this method, you can verify that with the callable function:

The callable function expects an object as a parameter and returns True when an object is a Callable object.

Note. Python defines several special methods that follow the format: __*__ , these methods are named magic method or double underscore method, a.k.a dunder method. In general, this is how is implemented the standard protocols on Python.

Extended this idea, is normal to infer that the methods (functions hooked to an object) are too callable objects, for example:

Functions and methods are the more basic callable objects, but they are not the only ones. Exists many other Callable objects, for example, the classes.

So, what are the consequences of having a callable object?. All the callable objects can be used in this way:

my_callable_obj(arg1, arg2, ...)

This is the reason, why a class like Person is Callable. When you “call” Person , you are passed some parameters that define the initial values of a new instance.

In the background, when you use the syntax for “call”, like: callable_obj(arg1, arg2, ...) you are invoking the dunder method __call__ with all the arguments, this method returns the result that you receive when you “call” that object.

Note. An important note is that all the callable object returns the None value if nothing is returned in an explicit way.

Create your custom callables

Photo by Xavi Cabrera on Unsplash

As you can see, Python defines many callable objects but you can create custom classes that return instances that are callable. The only requirement for creating a callable object is the implementation of the dunder method __call__ . For example, in the next example, we create an object that checks that a number is a positive integer value, this object can receive an optional max value as a constructor argument.

Use functions for simple interfaces

In other languages, you might expect hooks to be defined by an abstract class. In Python, many hooks are just stateless functions with well-defined arguments and return values. Functions work as hooks because Python has first-class functions

— Effective Python: 90 Specific Ways to Write Better Python

Using functions for simple interfaces can be a great idea for simplifying the design of your system. Exists many standard examples of this approach:

  1. Sort

sorted is a standard function that receives an Iterable and returns the sorted representation of it. By default, the sorted representation is created with the help of the < operator and the “key” of the object (by default is the object itself). You can add a special parameter key with a function that receives each object and returns what is the “key” of the object.

For example, you can sort a list of string by the length of the content.

l = ['aaa', 'aa', 'aaaa', 'a']
sorted(l, key=len)

The value of the key parameter should be a function (or other callable) that takes a single argument and returns a key to use for sorting purposes. This technique is fast because the key function is called exactly once for each input record.

Note. Is important to note that key=len has the same behavior that key=lambda obj: len(obj).

2. Filter

You can filter any Iterable by a custom condition with the filter function. The custom condition is defined as a Callable object that returns True when the object is going to be included in the result, or False when is going to be discarded.

from functools import filteris_active = lambda user: user.status == 'active'users = [ ... ]
active_users = filter(is_active, users)

3. Iterable tools

Following the example of the filter function, exists a bunch of functions with the same structure in the itertools package, for example:

from itertools import takewhile
from functools import partial
from operator import eq
file_tokens = [ ... ]
eof = 'end of file'
is_not_eof = partial(neq, eof)
valid_tokens = takewhile(is_not_eof, file_tokens)

To make it a more interesting example, I use two extra functions: partial and neq .

partial is a function that applies one or many arguments to a function and returns another function that can accept the rest of the argument that the original function expects.

neq(a, b) is a function that expects two arguments and returns True/False according to the equality; is the same that the != operator.

When things get complicated

Photo by Michal Matlon on Unsplash

What happened if, with time, the login is get more complex?. Simple, transform your function into a Callable object.

For example, if your initial function validates if a user can enter the system:

And with time, your logic gets more complex, you can create a custom class that returns instances that are Callable, for example:

Note. The all function receives an Iterable of boolean values and returns True only when all the arguments that are passed to it are True.

Other protocols

Callable is not the unique standard protocol that exists in Python, exists many ones, you can view a complete list of them in the documentation and you can see my other post that explains one of the most useful protocols, the Iterator, and Iterable protocols.

Conclusions

  • Callable is a standard protocol in Python.
  • You can check if an object implements the Callable protocol with the callable function.
  • Classes, functions, lambda functions, all of them are Callable objects.
  • To implement the Callable protocol, you must implement the __call__ method.
  • A simple function can represent a simple interface, that with time can be more complex and should be refactored to a custom Callable object.

Thanks for reading!

This is all for this post, I hope the content has been to your liking, see you in the next post.

More content at plainenglish.io. Sign up for our free weekly newsletter. Get exclusive access to writing opportunities and advice in our community Discord.

--

--