Function variations in Python
In Object Oriented Programming, we deal with classes and their variations. A subclass is conceptually a more concrete realization of it’s superclass. Subclasses appear to be a family of classes with certain extent of variation.
Variations are also introduced in functions. A function can derive a family of functions that are similar but with the same purpose. We will use Python functions as example to demonstrate the use of function variations, and effectively how it changes our way of writing clean code and tests.
Let’s assume we have a website that has four versions of different languages with English as major language.
translate function translate a English sentence into a sentence in target language (
to_language). It uses
tokenizer to split English sentence into words, find corresponding words in target language, then uses
composer to put them into a sentence.
Because we have four languages, each has corresponding
composer. We do not want the user of
translate function to actually initialize those arguments since it would be error-prone. Hence we write another four functions for users to use.
We find that the whole set of
translate_* function are merely creating parameters and call
translate. They have different implementations logic but are with exact same purpose.
For this kind of function variations, we could simplify them using
partial takes a original function and returns a new function. Calling new function is simply a call to original function with certain positional or keyword arguments set in advance.
translate_* function are exactly identical to those we created before.
partial function doesn’t necessarily save a lots a keystrokes, but brings some benefits for writing function variations of functions like
- Prevent addtional functionalities to be attached to
translate_*. They are a set of functions that are meant for a same purpose.
- You can skip testing function variations when you can test
- Could create cascading function variations (see below).
We have a function
inc that takes either a number or a list of number then return a number or a list with every number increased by given amount.
This is a very common use case, where you want to provide both versions for single object or a list of object, even a dictionary.
We can rewrite this function using decorator
singledispatch takes a look at the type of the type of the first argument when the decorated function is called, and call the right version for that type. It’s available since Python 3.4.
When the first positional argument is of type
__inc_list will be called by
__inc_list in turn calls
inc with first positional argument is a number. If the first positional argument is neither list nor number, the original
inc function will be called and triggers
singledispatch here created a family of function variations without any branch. Functionalities for each type could be maintained separately but still serving the same purpose. It’s also easiler to test thanks to the absence of branches.
Creating function variations using
singledispatch is interesting that variations can be consistently focused on a single purpose. This is very helpful for an evolving large scale system to provide a set of limited but varied interfaces, without introducing terrible complexity and frustrations.