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.

Partial functions

Let’s assume we have a website that has four versions of different languages with English as major language.

A 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
def translate(
sentence_in_english,
to_language,
language_dictionary,
tokenizer,
composer,
):
tokens = tokenizer(sentence_in_english)
tokens_translated = [
language_dictionary[token]
for token in tokens
]
return composer(tokens_translated).to_string()

Because we have four languages, each has corresponding language_dictionary, tokenizer and 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def translate_mandarin(sentence_in_english):
language_dictionary = LanguageDictionary(...)
tokenizer = MandarinTokenizer(...)
composer = MandarinComoser(...)
return translate(
sentence_in_english,
'mandarin',
language_dictionary,
tokenizer,
composer,
)

def translate_german(sentence_in_english):
...

def translate_cantonese(sentence_in_english):
...

def translate_japanese(sentence_in_english):
...

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 functools.partial. 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.

1
2
3
4
5
6
7
8
9
10
11
12
from functools import partial

translate_mandarin = partial(
translate,
to_language='mandarin',
language_dictionary=MandarinDictionary(...),
tokenizer=MandarinTokenizer(...),
composer=MandarinComoser(...),
)
translate_german = partial(translate, ...)
translate_cantonese = partial(translate, ...)
translate_japanese = partial(translate, ...)

Here translate_* function are exactly identical to those we created before.

Using partial function doesn’t necessarily save a lots a keystrokes, but brings some benefits for writing function variations of functions like translate:

  1. Prevent addtional functionalities to be attached to translate_*. They are a set of functions that are meant for a same purpose.
  2. You can skip testing function variations when you can test tranlate throughly.
  3. Could create cascading function variations (see below).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# `ChineseTokenizer`, `ChineseComposer` could be used for either mandarin and cantonese
translate_chinese = partial(
translate,
tokenizer=ChineseTokenizer(...),
composer=ChineseComposer(...),
)
translate_mandarin = partial(
translate_chinese,
to_language='mandarin',
language_dictionary=MandarinDictionary(...),
)
translate_cantonese = partial(
translate_chinese,
to_language='cantonese',
language_dictionary=CantoneseDictionary(...),
)

Dispatch functions

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.

1
2
3
4
5
6
7
8
9
10
def inc(obj, amount):
if isinstance(obj, numbers.Number):
return obj + amount
elif isinstance(obj, list):
return [inc(item, amount) for item in obj]
else:
raise TypeError()

assert inc(1, 2) == 3
assert inc([1, 2], 2) == [3, 4]

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 functools.singledispatch. 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import singledispatch

@singledispatch
def inc(other_types_arg, amount):
raise TypeError()

@inc.register(numbers.Number)
def __inc_number(number, amount):
return number + amount

@inc.register(list)
def ___inc_list(list_of_number, amount):
return [inc(number, amount) for number in list_of_number]

assert inc(1, 2) == 3
assert inc([1, 2], 2) == [3, 4]

When the first positional argument is of type list, __inc_list will be called by singledispatch. __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 TypeError.

Usage of 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.

Conclusion

Creating function variations using partial and 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.