- Published on
functools.singledispatch. Simple solution to dispatch on type.
- Authors
- Name
- Piotr Kurnik
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 elif
s. 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 tomulti-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