Welcome, guest | Sign In | My Account | Store | Cart

The recipe presents a function overloading decorator in python that do type check. The type signature is marked with the @takes and @returns decorator, which causes the function to raise an InputParameterError exception if called with inappropriate arguments. The @overloaded function searches for the first overloads that doesn't raise TypeError or InputParameterError when called. Alternative overloads are added to the overloads list by using the @func.overload_with decorator. The order of function definition determines which function gets tried first and once it founds a compatible function, it skips the rest of the overloads list.

Python, 101 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#!/usr/bin/env python

from functools import wraps


def returns(rettype):
    def check(ret):
        if not isinstance(ret, rettype): raise InputParameterError()
        return ret
    def returnchecker(func):
        @wraps(func)
        def _func(*args, **kwargs):
            return check(func(*args, **kwargs))

        _func.returns = rettype
        return _func
    return returnchecker

def takes(*argtypes, **kwtypes):
    def check(args, kwargs):
        if not len(args) == len(argtypes): raise InputParameterError()
        if not all(isinstance(a, b) for a, b in zip(args, argtypes)): raise InputParameterError()

        if not len(kwargs) == len(kwtypes): raise InputParameterError()
        if not set(kwargs) == set(kwtypes) : raise InputParameterError()
        if not all(isinstance(kwargs[kw], kwtypes[kw]) for kw in kwtypes): raise InputParameterError()

    def typechecker(func):
        @wraps(func)
        def _func(*args, **kwargs):
            check(args, kwargs)
            return func(*args, **kwargs)

        _func.signature = argtypes, kwtypes
        return _func
    return typechecker

## note: there is a more comprehensive signature checking recipe #426123
## http://code.activestate.com/recipes/426123-method-signature-checking-decorators/
## this recipe is compatible with that recipe, simply copy that recipe to 
## "signature.py" and uncomment the following import lines ##
##
## from signature import *

class InputParameterError(Exception): pass
def overloaded(func):
    @wraps(func)
    def overloaded_func(*args, **kwargs):
        for f in overloaded_func.overloads:
            try:
                return f(*args, **kwargs)
            except (InputParameterError, TypeError):
                pass
        else:
            raise TypeError("No compatible signatures")

    def overload_with(func):
        overloaded_func.overloads.append(func)
        return overloaded_func
    overloaded_func.overloads = [func]
    overloaded_func.overload_with = overload_with
    return overloaded_func

#############


if __name__ == '__main__':
    @overloaded
    def a():
        print 'no args a'
        pass
    @a.overload_with
    def a(n):
        print 'arged a'
        pass

    a()
    a(4)

    @overloaded
    @returns(int)
    @takes(int, int, float)
    def foo(a, b, c):
        return int(a * b * c)

    @foo.overload_with
    @returns(int)
    @takes(int, float, int, int)
    def foo(a, b, c, d):
        return int(a + b + c)

    @foo.overload_with
    @returns(int)
    @takes(int, float, c=int)
    def foo(a, b, c):
        return int(a + b + c)

    print foo(2, 3, 4.)
    print foo(10, 3., c=30)
    print foo(1, 9., 3, 3)
    print foo('string')

In the past, tricks using default arguments such as:

def foo(n=None):
    if n is None: n = 10
    return n + 20

has been used and has become a popular idiom; this decorator is meant to replace a subset of that idiom.

Using function overloading cleanly separates the code for each signature and prevents code in one signature from interfering code in another signature. It also makes for smaller function body, as each function only cares about its own signature.

Known Issues: The decorator relies on catching TypeError, therefore if the underlying code raises TypeError... nobody knows what might happen.

Python 3: Python 3's new type-annotation syntax would be a more convenient place to define the signature.

This recipe is the type checking version of the recipe here: http://code.activestate.com/recipes/577064-simple-function-overloading-with-decorator/

2 comments

Gabriel Genellina 11 years, 9 months ago  # | flag

There is a hidden, single-argument version of this recipe already in pkgutil.simplegeneric - see http://bugs.python.org/issue5135 . And a full featured package of the same name (same author) on PyPi. This recipe is fine as it is and lies in between those two.

alfredp.bartholomai 11 years, 2 months ago  # | flag

This is a great recipe. I would like to be able to use it for overloading methods within a python class. Will that work ? I tried it in a simple class trying to overload a few methods and I kept getting the error: raise TypeError("No compatible signatures") TypeError: No compatible signatures

Any help or suggestions or an example would be much appreciated.

Thank you