Published on

functools.singledispatch. Simple solution to dispatch on type.

Authors
  • avatar
    Name
    Piotr Kurnik
    Twitter

What problem are we trying to solve?

There are times when you want to create some kind of action depending on the type of the input argument. You would performa different action if user was of RegularUser class, and different action when user is SuperUser. Such cases are often connected with inheritance of classes.
There are many ways you can address this issue. I haven’t even listed all the possible solutions below. The main goal of this post is to let you know that Python Standard library has solution for you. It’s simple, clean and does not require any dependencies.

Here is basic class hierarchy for this code example. It’s simplistic as it is a contrived example, but it’s enough to get the main point.

class BaseUser:
    def __init__(self, username, email, password):
        self._username = username
        self._email = email
        self._password = password
        self._is_admin = False
        self._is_super_user = False

    @property
    def is_admin(self):
        return self._is_admin

    @property
    def is_super_user(self):
        return self._is_super_user

    @property
    def username(self):
        return self._username

    @property
    def password(self):
        return "*"* len(self._password)


class RegularUser(BaseUser):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._is_admin = False


class AdminUser(BaseUser):

    def __init__(self, *args, **kwargs):
        super(AdminUser, self).__init__(*args, **kwargs)
        self._is_admin = True


class SuperUser(AdminUser):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._is_super_user = True

Using if-statements

Yup, it works. I just don’t think it’s particularly elegant. I am put of by repeated elif statements. It works for simple things perhaps, but for more complicated cases it’s just ugly.

def greet_user(user) -> str:
    if isinstance(user, RegularUser):
        return f"Hello {user.username}, password is {user.password}"
    elif isinstance(user, AdminUser):
        return f"Hello Administrator {user.username}, password is {user.password}"
    elif isinstance(user, SuperUser):
        return f"Hello SpeerUser {user.username}, password is {user.password}"
    else:
        return f"I am the default action for {user.username}"


if __name__ == '__main__':
    regular_user = RegularUser(username="User1", email="user@example.com", password="TopSecret1")
    admin_user = AdminUser(username="Admin", email="administrator@example.com", password="TopSecretAdminPassword")
    super_admin = SuperUser(username="SuperArmin", email="super-administrator@example.com",
                            password="SuperAdministrator")

    print(greet_user(regular_user))
    print(greet_user(admin_user))
    print(greet_user(super_admin))

In this case we have simple strings returned by the if statements. If it was something more complicated, in place of each return statement there would have to be a function call. We will come back to that point soon.

Dictionary dispatch

def greet(user: BaseUser) -> str:
    strategy = {
        RegularUser: lambda user: f"Hello {user.username}, password is {user.password}",
        AdminUser: lambda user: f"Hello Administrator {user.username}, password is {user.password}",
        SuperUser: lambda user: f"Hello SuperUser {user.username}, password is {user.password}"
    }
    return strategy.get(type(user))(user)


if __name__ == '__main__':
    regular_user = RegularUser(username="User1", email="user@example.com", password="TopSecret1")
    admin_user = AdminUser(username="Admin", email="administrator@example.com", password="TopSecretAdminPassword")
    super_admin = SuperUser(username="SuperArmin", email="super-administrator@example.com",
                            password="SuperAdministrator")
    print(greet(user=regular_user))
    print(greet(user=admin_user))
    print(greet(user=super_admin))

Using dictionary is, in my opinion, much more aesthetically appealing than multiple elifs. Same point as above; this case involves a simple string as a return value . Had it been something more complex, there would be function call a the point of each return. That brings us to the whole point of using functools.singledispatch.

The point is this:

Since we have to implement a function for every case, based on type of the input argument, we may just as well not worry about the logic used to choose which implementation should we run, given the input. We can leave it up to the functors.singledispatch and focus on implementation of each case.

from functools import singledispatch


@singledispatch
def greet_user(user):
    return f"I am the default action for {user.username}"

@greet_user.register(RegularUser)
def _(user):
    return f"Hello {user.username}, password is {user.password}"

@greet_user.register(AdminUser)
def _(user):
    return f"Hello Administrator {user.username}, password is {user.password}"

@greet_user.register(SuperUser)
def _(user):
    return f"Hello SuperUser {user.username}, password is {user.password}"

if __name__ == '__main__':
    regular_user = RegularUser(username="User1", email="user@example.com", password="TopSecret1")
    admin_user = AdminUser(username="Admin", email="administrator@example.com", password="TopSecretAdminPassword")
    super_admin = SuperUser(username="SuperArmin", email="super-administrator@example.com", password="SuperAdministrator")

    print(greet_user(regular_user))
    print(greet_user(admin_user))
    print(greet_user(super_admin))

That is exactly what is going on in the example above. Every case that we want to cover is a separate function. There are a couple of points:

  • Default case is always the first function. In our case it’s:
@singledispatch
def greet_user(user):
    return f"I am the default action for {user.username}"

  • greeet_user is a /generic function/ which is a function that implements the same operation for different type of the input argument
  • Dispatch algorithm is a fancy way of saying: what is the logic behind the choice of the correct implementation of given functionality
  • Single dispatch, means that the dispatch algorithm depends on single argument of the function. As you can see, the function greet_user accepts single argument, user which is then used by every single overload.
  • To make a generic function you have to decorate it with singledispatch decorator
  • Overloads are versions of the functionality for given type of the user.
  • Every overload is created by creating a function and decorating it with generic_function_name.register decorator.
  • Overloads are called _. You can call them whatever you want, but the convention is to call them _. It makes sense, since the only thing that is important is the context behind what you are doing.
  • The concept of singledispatch in Python is very similar to multi-methods in Clojure. Since Clojure is very cool language, I recommend that you give it a go.

Summary

functools.singledispatch is a very nice alternative to dictionary-based switch emulation or a pile of if-statements. It allows you to focus on business logic, while leaving the dispatch mechanism to the Python standard library. That is what you want, focus on added value, not necesarilly mechanics Code examples can be accessed GitHub - PiotrKurnik/singledispatch