我知道Python不支持方法重载,但我遇到了一个问题,我似乎无法用Python的好方法来解决。

我正在创造一款角色需要射击各种子弹的游戏,但是我该如何编写不同的函数去创造这些子弹呢?例如,假设我有一个函数,它创建了一颗以给定速度从a点飞到B点的子弹。我会这样写一个函数:

def add_bullet(sprite, start, headto, speed):
    # Code ...

但我想写其他函数来创建项目符号,比如:

def add_bullet(sprite, start, direction, speed):
def add_bullet(sprite, start, headto, spead, acceleration):
def add_bullet(sprite, script): # For bullets that are controlled by a script
def add_bullet(sprite, curve, speed): # for bullets with curved paths
# And so on ...

等等,有很多变化。有没有更好的方法不用这么多关键字参数,因为它很快就会变得很难看。重命名每个函数也很糟糕,因为你得到的不是add_bullet1、add_bullet2就是add_bullet_with_really_long_name。

以下是一些问题的答案:

不,我不能创建一个子弹类层次结构,因为那太慢了。管理项目符号的实际代码是用C编写的,我的函数是围绕C API的包装器。 我知道关键字参数,但检查各种参数组合是令人讨厌的,但默认参数帮助分配,如加速度=0


当前回答

要么在定义中使用多个关键字参数,要么创建一个Bullet层次结构,将其实例传递给函数。

其他回答

你要求的是所谓的多重调度。参见Julia语言示例,其中演示了不同类型的分派。

然而,在讨论这个问题之前,我们首先要解决为什么在Python中重载并不是你真正想要的。

为什么不超载?

首先,我们需要理解重载的概念,以及为什么它不适用于Python。

在使用可以区分数据类型的语言时 编译时,可以在 编译时。为…创造这样的替代功能的行为 编译时选择通常称为重载 函数。(维基百科)

Python是一种动态类型语言,因此重载的概念并不适用于它。然而,并不是所有的都失去了,因为我们可以在运行时创建这样的替代函数:

在编程语言中,将数据类型识别推迟到 运行时在备选项中进行选择 函数必须在运行时根据动态确定的值发生 函数参数的类型。其替代函数 以这种方式选择的实现引用最多 通常称为多方法。(维基百科)

因此,我们应该能够在python中使用多方法——或者,也可以称为:多分派。

多分派

多方法也被称为多重调度:

多调度或多方法是一些的特点 面向对象的程序设计语言,其中包含函数或方法 的运行时(动态)类型可以动态分派 不止一个论点。(维基百科)

Python不支持开箱即用1,但是,恰好有一个名为multipledispatch的优秀Python包可以做到这一点。

解决方案

下面是我们如何使用multipledispatch2包来实现你的方法:

>>> from multipledispatch import dispatch
>>> from collections import namedtuple
>>> from types import *  # we can test for lambda type, e.g.:
>>> type(lambda a: 1) == LambdaType
True

>>> Sprite = namedtuple('Sprite', ['name'])
>>> Point = namedtuple('Point', ['x', 'y'])
>>> Curve = namedtuple('Curve', ['x', 'y', 'z'])
>>> Vector = namedtuple('Vector', ['x','y','z'])

>>> @dispatch(Sprite, Point, Vector, int)
... def add_bullet(sprite, start, direction, speed):
...     print("Called Version 1")
...
>>> @dispatch(Sprite, Point, Point, int, float)
... def add_bullet(sprite, start, headto, speed, acceleration):
...     print("Called version 2")
...
>>> @dispatch(Sprite, LambdaType)
... def add_bullet(sprite, script):
...     print("Called version 3")
...
>>> @dispatch(Sprite, Curve, int)
... def add_bullet(sprite, curve, speed):
...     print("Called version 4")
...

>>> sprite = Sprite('Turtle')
>>> start = Point(1,2)
>>> direction = Vector(1,1,1)
>>> speed = 100 #km/h
>>> acceleration = 5.0 #m/s**2
>>> script = lambda sprite: sprite.x * 2
>>> curve = Curve(3, 1, 4)
>>> headto = Point(100, 100) # somewhere far away

>>> add_bullet(sprite, start, direction, speed)
Called Version 1

>>> add_bullet(sprite, start, headto, speed, acceleration)
Called version 2

>>> add_bullet(sprite, script)
Called version 3

>>> add_bullet(sprite, curve, speed)
Called version 4

1. Python 3目前支持单分派 2. 注意不要在多线程环境中使用multipledispatch,否则会出现奇怪的行为。

一个可能的选项是使用multipledispatch模块,如下所示: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch

不要这样做:

def add(self, other):
    if isinstance(other, Foo):
        ...
    elif isinstance(other, Bar):
        ...
    else:
        raise NotImplementedError()

你可以这样做:

from multipledispatch import dispatch
@dispatch(int, int)
def add(x, y):
    return x + y    

@dispatch(object, object)
def add(x, y):
    return "%s + %s" % (x, y)

使用结果的用法:

>>> add(1, 2)
3

>>> add(1, 'hello')
'1 + hello'

通过传递关键字args。

def add_bullet(**kwargs):
    #check for the arguments listed above and do the proper things

Python 3.8增加了functools.singledispatchmethod

将方法转换为单分派泛型函数。 要定义一个泛型方法,请使用@singledispatchmethod装饰它 装饰。注意,调度发生在第一个的类型上 非self或非cls参数,相应地创建你的函数:

from functools import singledispatchmethod


class Negator:
    @singledispatchmethod
    def neg(self, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(self, arg: int):
        return -arg

    @neg.register
    def _(self, arg: bool):
        return not arg


negator = Negator()
for v in [42, True, "Overloading"]:
    neg = negator.neg(v)
    print(f"{v=}, {neg=}")

输出

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a

@singledispatchmethod支持与其他装饰器嵌套,例如 @classmethod。注意,为了允许dispatcher.register, Singledispatchmethod必须是最外层的装饰器。这是 带有negg方法的否定类:

from functools import singledispatchmethod


class Negator:
    @singledispatchmethod
    @staticmethod
    def neg(arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(arg: int) -> int:
        return -arg

    @neg.register
    def _(arg: bool) -> bool:
        return not arg


for v in [42, True, "Overloading"]:
    neg = Negator.neg(v)
    print(f"{v=}, {neg=}")

输出:

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a

相同的图案可以用于其他类似的装饰: Staticmethod、abstractmethod等。

@overload装饰器添加了类型提示(PEP 484)。

虽然这并没有改变Python的行为,但它确实使它更容易理解正在发生的事情,并使mypy检测错误。

参见:输入提示和PEP 484