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.
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 | def translate( |
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 | def translate_mandarin(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 | from functools import partial |
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
:
- 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
tranlate
throughly. - Could create cascading function variations (see below).
1 | # `ChineseTokenizer`, `ChineseComposer` could be used for either mandarin and cantonese |
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 | def inc(obj, 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 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 | from functools import singledispatch |
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.
让有趣易懂的知识主动找到你
订阅我的Email半月刊,让我们共同学习、成长。绝无广告!