Quick answer: PyPI has an overload package that implements this more reliably than what I describe below, although using slightly different syntax. He announced that he only works with Python 3, but it seems that only minor modifications will be required to work with Python 2 (if any, I have not tried).
Long answer:. In languages ββwhere you can overload functions, the name of the function (literally or effectively) is supplemented by information about its type signature, both when defining a function and when it is called. When the compiler or interpreter looks at the definition of a function, then it uses both the declared name and parameter types to allow access to this function. Thus, the logical way to implement overloading in Python is to implement a shell that uses both the declared name and parameter types to solve this function.
Here's a simple implementation:
from collections import defaultdict def determine_types(args, kwargs): return tuple([type(a) for a in args]), \ tuple([(k, type(v)) for k,v in kwargs.iteritems()]) function_table = defaultdict(dict) def overload(arg_types=(), kwarg_types=()): def wrap(func): named_func = function_table[func.__name__] named_func[arg_types, kwarg_types] = func def call_function_by_signature(*args, **kwargs): return named_func[determine_types(args, kwargs)](*args, **kwargs) return call_function_by_signature return wrap
overload should be called with two optional arguments, a tuple representing the types of all positional arguments and tuples of the tuples representing matching name types of all the keyword arguments. Here is a usage example:
>>> @overload((str, int)) ... def f(a, b): ... return a * b >>> @overload((int, int)) ... def f(a, b): ... return a + b >>> print f('a', 2) aa >>> print f(4, 2) 6 >>> @overload((str,), (('foo', int), ('bar', float))) ... def g(a, foo, bar): ... return foo*a + str(bar) >>> @overload((str,), (('foo', float), ('bar', float))) ... def g(a, foo, bar): ... return a + str(foo*bar) >>> print g('a', foo=7, bar=4.4) aaaaaaa4.4 >>> print g('b', foo=7., bar=4.4) b30.8
Disadvantages of this include
In fact, he does not verify that the function on which the decorator is applied is even compatible with the arguments provided to the decorator. You can write
@overload((str, int)) def h(): return 0
and you will get an error when calling the function.
It does not gracefully handle the case where there is no overloaded version corresponding to the types of arguments passed (this would help to increase a more descriptive error)
It distinguishes between named and positional arguments, so something like
g('a', 7, bar=4.4)
does not work.
- There are many nested parentheses in use, as in the definitions for
g . - As mentioned in the comments, this does not apply to functions with the same name in different modules.
All of this could be corrected with enough grunts, I think. In particular, the name collision problem is easily resolved by storing the distribution table as an attribute of the function returned from the decorator. But, as I said, this is just a simple example that demonstrates the basics of how to do this.