I have a large existing program library that currently has a .NET binding, and I\'m thinking about writing a Python binding. The existing API makes extensive use of signatur
One way would be to just write code parse the args yourself. Then you wouldn't have to change the API at all. You could even write a decorator so it'd be reusable:
import functools
def overload(func):
'''Creates a signature from the arguments passed to the decorated function and passes it as the first argument'''
@functools.wraps(func)
def inner(*args):
signature = tuple(map(type, args))
return func(signature, *args)
return inner
def matches(collection, sig):
'''Returns True if each item in collection is an instance of its respective item in signature'''
if len(sig)!=len(collection):
return False
return all(issubclass(i, j) for i,j in zip(collection, sig))
@overload
def Circle1(sig, *args):
if matches(sig, (Point,)*3):
#do stuff with args
print "3 points"
elif matches(sig, (Point, float)):
#as before
print "point, float"
elif matches(sig, (Curve,)*3):
#and again
print "3 curves"
else:
raise TypeError("Invalid argument signature")
# or even better
@overload
def Circle2(sig, *args):
valid_sigs = {(Point,)*3: CircleThroughThreePoints,
(Point, float): CircleCenterRadius,
(Curve,)*3: CircleTangentThreeCurves
}
try:
return (f for s,f in valid_sigs.items() if matches(sig, s)).next()(*args)
except StopIteration:
raise TypeError("Invalid argument signature")
This is the best part. To an API user, they just see this:
>>> help(Circle)
Circle(*args)
Whatever's in Circle's docstring. You should put info here about valid signatures.
They can just call Circle
like you showed in your question.
The whole idea is to hide the signature-matching from the API. This is accomplished by using a decorator to create a signature, basically a tuple containing the types of each of the arguments, and passing that as the first argument to the functions.
When you decorate a function with @overload
, overload
is called with that function as an argument. Whatever is returned (in this case inner
) replaces the decorated function. functools.wraps
ensures that the new function has the same name, docstring, etc.
Overload is a fairly simple decorator. All it does is make a tuple of the types of each argument and pass that tuple as the first argument to the decorated function.
This is the simplest approach. At the beginning of the function, just test the signature against all valid ones.
This is a little more fancy. The benefit is that you can define all of your valid signatures together in one place. The return statement uses a generator to filter the matching valid signature from the dictionary, and .next()
just gets the first one. Since that entire statement returns a function, you can just stick a ()
afterwards to call it. If none of the valid signatures match, .next()
raises a StopIteration
.
All in all, this function just returns the result of the function with the matching signature.
One thing you see a lot in this bit of code is the *args
construct. When used in a function definition, it just stores all the arguments in a list named "args". Elsewhere, it expands a list named args
so that each item becomes an argument to a function (e.g. a = func(*args)
).
I don't think it's terribly uncommon to do odd things like this to present clean APIs in Python.