Overloading (or alternatives) in Python API design

后端 未结 5 1981
半阙折子戏
半阙折子戏 2021-01-03 02:07

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

5条回答
  •  悲&欢浪女
    2021-01-03 02:23

    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")
    

    How it appears to API users:

    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.

    How it works:

    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.

    overload:

    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.

    Circle take 1:

    This is the simplest approach. At the beginning of the function, just test the signature against all valid ones.

    Circle take 2:

    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.

    final notes:

    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.

提交回复
热议问题