Python API设计中的重载(或替代)

时间:2014-08-16 02:46:22

标签: python overloading

我有一个现有的大型程序库,目前有一个.NET绑定,我正在考虑编写一个Python绑定。现有API广泛使用基于签名的重载。所以,我有很多静态函数集合,如:

Circle(p1, p2, p3) -- Creates a circle through three points
Circle(p, r)       -- Creates a circle with given center point and radius
Circle(c1, c2, c3) -- Creates a circle tangent to three curves

在某些情况下,必须以不同的方式使用相同的输入,因此基于签名的重载不起作用,而我必须使用不同的函数名称。例如

BezierCurve(p1,p2,p3,p4) -- Bezier curve using given points as control points
BezierCurveThroughPoints(p1,p2,p3,p4) -- Bezier curve passing through given points

我认为第二种技术(使用不同的函数名称)可以在Python API中的任何地方使用。所以,我会

CircleThroughThreePoints(p1, p2, p3)
CircleCenterRadius(p, r)
CircleTangentThreeCurves(c1, c2, c3)

但这些名字看起来令人不快(我不喜欢缩写),而发明所有这些名字将是一个非常大的挑战,因为图书馆有数以千计的功能。

低优先级:
努力(就我而言) - 如果我必须写很多代码,我都不在乎 性能

高优先级:
易于使用/理解呼叫者(许多人将编程新手) 我很容易写好文档。
简单 - 避免在调用者代码中使用高级概念。

我确定我不是第一个希望在Python中使用基于签名的重载的人。人们通常使用哪些解决方法?

5 个答案:

答案 0 :(得分:8)

一个选项是在构造函数中排除关键字参数,并包含逻辑以确定应该使用的内容:

class Circle(object):
    def __init__(self, points=(), radius=None, curves=()):
        if radius and len(points) == 1:
            center_point = points[0]
            # Create from radius/center point
        elif curves and len(curves) == 3:
            # create from curves
        elif points and len(points) == 3:
            # create from points
        else:
            raise ValueError("Must provide a tuple of three points, a point and a radius, or a tuple of three curves)

您还可以使用classmethods为API的用户提供便利:

class Circle(object):
    def __init__(self, points=(), radius=None, curves=()):
         # same as above

    @classmethod
    def from_points(p1, p2, p3):
        return cls(points=(p1, p2, p3))

    @classmethod
    def from_point_and_radius(cls, point, radius):
        return cls(points=(point,), radius=radius)

    @classmethod
    def from_curves(cls, c1, c2, c3):
        return cls(curves=(c1, c2, c3))

用法:

c = Circle.from_points(p1, p2, p3)
c = Circle.from_point_and_radius(p1, r)
c = Circle.from_curves(c1, c2, c3)

答案 1 :(得分:5)

有几种选择。

您可以拥有一个接受任意数量的参数(使用*args和/或**varargs语法)的构造函数,并根据参数的数量和类型执行不同的操作。

或者,您可以将辅助构造函数编写为类方法。这些被称为“工厂”方法。如果您有多个构造函数,它们使用相同类的相同数量的对象(如BezierCurve示例中所示),这可能是您唯一的选择。

如果您不介意覆盖__new__而不是__init__,您甚至可以同时使用__new__方法处理一种形式的参数,并将其他类型引用到工厂正规化方法。以下是可能的示例,包括__new__多个签名的文档字符串:

class Circle(object):
    """Circle(center, radius) -> Circle object
       Circle(point1, point2, point3) -> Circle object
       Circle(curve1, curve2, curve3) -> Circle object

       Return a Circle with the provided center and radius. If three points are given,
       the center and radius will be computed so that the circle will pass through each
       of the points. If three curves are given, the circle's center and radius will
       be chosen so that the circle will be tangent to each of them."""

    def __new__(cls, *args):
        if len(args) == 2:
            self = super(Circle, cls).__new__(cls)
            self.center, self.radius = args
            return self
        elif len(args) == 3:
            if all(isinstance(arg, Point) for arg in args):
                return Circle.through_points(*args)
            elif all(isinstance(arg, Curve) for arg in args):
                return Circle.tangent_to_curves(*args)
        raise TypeError("Invalid arguments to Circle()")

    @classmethod
    def through_points(cls, point1, point2, point3):
        """from_points(point1, point2, point3) -> Circle object

        Return a Circle that touches three points."""

        # compute center and radius from the points...
        # then call back to the main constructor:
        return cls(center, radius)

    @classmethod
    def tangent_to_curves(cls, curve1, curve2, curve3):
        """from_curves(curve1, curve2, curve3) -> Circle object

        Return a Circle that is tangent to three curves."""

        # here too, compute center and radius from curves ...
        # then call back to the main constructor:
        return cls(center, radius)

答案 2 :(得分:2)

您可以使用字典,如此

Circle({'points':[p1,p2,p3]})
Circle({'radius':r})
Circle({'curves':[c1,c2,c3])

初始化程序会说

def __init__(args):
  if len(args)>1:
    raise SomeError("only pass one of points, radius, curves")
  if 'points' in args: {blah}
  elsif 'radius' in args: {blahblah}
  elsif 'curves' in args: {evenmoreblah}
  else: raise SomeError("same as above")

答案 3 :(得分:2)

一种方法是自己编写代码解析args。那么你根本不需要改变API。你甚至可以写一个装饰器,以便它可以重复使用:

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

API用户看起来如何:

这是最好的部分。对于API用户,他们只看到:

>>> help(Circle)

Circle(*args)
  Whatever's in Circle's docstring. You should put info here about valid signatures.

他们可以像您在问题中所显示的那样致电Circle

工作原理:

整个想法是隐藏API的签名匹配。这是通过使用decorator来创建签名,基本上是包含每个参数类型的元组,并将其作为函数的第一个参数传递来实现的。

过载:

使用@overload修饰函数时,使用该函数作为参数调用overload。无论返回什么(在本例中为inner)都会替换已修饰的函数。 functools.wraps确保新函数具有相同的名称,文档字符串等。

重载是一个相当简单的装饰器。它所做的就是创建每个参数类型的元组,并将该元组作为装饰函数的第一个参数传递。

圆圈取1:

这是最简单的方法。在函数的开头,只需针对所有有效的签名测试签名。

圆圈取2:

这有点花哨。好处是您可以在一个地方一起定义所有有效签名。 return语句使用生成器从字典中过滤匹配的有效签名,.next()只获取第一个。由于整个语句返回一个函数,因此您可以在之后粘贴()来调用它。如果所有有效签名都不匹配,.next()会引发StopIteration

总而言之,这个函数只返回带有匹配签名的函数的结果。

最后的笔记:

你在这段代码中看到很多东西的是*args构造。在函数定义中使用时,它只将所有参数存储在名为" args"的列表中。在其他地方,它会扩展名为args的列表,以便每个项目成为函数的参数(例如a = func(*args))。

我不认为做这样奇怪的事情在Python中提供干净的API非常罕见。

答案 4 :(得分:2)

PyPI中有许多模块可以帮助您进行基于签名的重载和调度:multipledispatchmultimethodsDispatching - 我没有真正的经验,但是multipledispatch看起来像你想要的,并且记录得很好。使用您的圆圈示例:

from multipledispatch import dispatch

class Point(tuple):
    pass

class Curve(object):         
    pass

@dispatch(Point, Point, Point)
def Circle(point1, point2, point3):
    print "Circle(point1, point2, point3): point1 = %r, point2 = %r, point3 = %r" % (point1, point2, point3)

@dispatch(Point, int)
def Circle(centre, radius):
    print "Circle(centre, radius): centre = %r, radius = %r" % (centre, radius)

@dispatch(Curve, Curve, Curve)
def Circle(curve1, curve2, curve3):
    print "Circle(curve1, curve2, curve3): curve1 = %r, curve2 = %r, curve3 = %r" % (curve1, curve2, curve3)


>>> Circle(Point((10,10)), Point((20,20)), Point((30,30)))
Circle(point1, point2, point3): point1 = (10, 10), point2 = (20, 20), point3 = (30, 30)
>>> p1 = Point((25,10))
>>> p1
(10, 10)
>>> Circle(p1, 100)
Circle(centre, radius): centre = (25, 10), radius = 100

>>> Circle(*(Curve(),)*3)
Circle(curve1, curve2, curve3): curve1 = <__main__.Curve object at 0xa954d0>, curve2 = <__main__.Curve object at 0xa954d0>, curve3 = <__main__.Curve object at 0xa954d0>

>>> Circle()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 143, in __call__
    func = self.resolve(types)
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
    (self.name, str_signature(types)))
NotImplementedError: Could not find signature for Circle: <>

它也可以装饰实例方法,因此你可以提供__init__()的多个实现,这非常好。如果您在班级中实施任何实际行为,例如Circle.draw(),您需要一些逻辑来确定绘制圆的可用值(中心和半径,3个点等)。但由于这只是提供一组绑定,您可能只需要调用正确的本机代码函数并传递参数:

from numbers import Number
from multipledispatch import dispatch

class Point(tuple):
    pass

class Curve(object):
    pass

class Circle(object):
    "A circle class"

    # dispatch(Point, (int, float, Decimal....))
    @dispatch(Point, Number)
    def __init__(self, centre, radius):
        """Circle(Point, Number): create a circle from a Point and radius."""

        print "Circle.__init__(): centre %r, radius %r" % (centre, radius)

    @dispatch(Point, Point, Point)
    def __init__(self, point1, point2, point3):
        """Circle(Point, Point, Point): create a circle from 3 points."""

        print "Circle.__init__(): point1 %r, point2 %r, point3 = %r" % (point1, point2, point3)

    @dispatch(Curve, Curve, Curve)
    def __init__(self, curve1, curve2, curve3):
        """Circle(Curve, Curve, Curve): create a circle from 3 curves."""

        print "Circle.__init__(): curve1 %r, curve2 %r, curve3 = %r" % (curve1, curve2, curve3)

    __doc__ = '' if __doc__ is None else '{}\n\n'.format(__doc__)
    __doc__ += '\n'.join(f.__doc__ for f in __init__.funcs.values())


>>> print Circle.__doc__
A circle class

Circle(Point, Number): create a circle from a Point and radius.
Circle(Point, Point, Point): create a circle from 3 points.
Circle(Curve, Curve, Curve): create a circle from 3 curves.

>>> for num in 10, 10.22, complex(10.22), True, Decimal(100):
...     Circle(Point((10,20)), num)
... 
Circle.__init__(): centre (10, 20), radius 10
<__main__.Circle object at 0x1d42fd0>
Circle.__init__(): centre (10, 20), radius 10.22
<__main__.Circle object at 0x1e3d890>
Circle.__init__(): centre (10, 20), radius (10.22+0j)
<__main__.Circle object at 0x1d42fd0>
Circle.__init__(): centre (10, 20), radius True
<__main__.Circle object at 0x1e3d890>
Circle.__init__(): centre (10, 20), radius Decimal('100')
<__main__.Circle object at 0x1d42fd0>

>>> Circle(Curve(), Curve(), Curve())
Circle.__init__(): curve1 <__main__.Curve object at 0x1e3db50>, curve2 <__main__.Curve object at 0x1d42fd0>, curve3 = <__main__.Curve object at 0x1d4b1d0>
<__main__.Circle object at 0x1d4b4d0>

>>> p1=Point((10,20))
>>> Circle(*(p1,)*3)
Circle.__init__(): point1 (10, 20), point2 (10, 20), point3 = (10, 20)
<__main__.Circle object at 0x1e3d890>

>>> Circle()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 235, in __call__
    func = self.resolve(types)
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
    (self.name, str_signature(types)))
NotImplementedError: Could not find signature for __init__: <>