我知道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


当前回答

一个可能的选项是使用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'

其他回答

对于函数重载,可以使用“自己动手”的解决方案。下面这个摘自Guido van Rossum关于多方法的文章(因为在Python中,多方法和重载之间几乎没有区别):

registry = {}

class MultiMethod(object):
    def __init__(self, name):
        self.name = name
        self.typemap = {}
    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args) # a generator expression!
        function = self.typemap.get(types)
        if function is None:
            raise TypeError("no match")
        return function(*args)
    def register(self, types, function):
        if types in self.typemap:
            raise TypeError("duplicate registration")
        self.typemap[types] = function


def multimethod(*types):
    def register(function):
        name = function.__name__
        mm = registry.get(name)
        if mm is None:
            mm = registry[name] = MultiMethod(name)
        mm.register(types, function)
        return mm
    return register

它的用法是

from multimethods import multimethod
import unittest

# 'overload' makes more sense in this case
overload = multimethod

class Sprite(object):
    pass

class Point(object):
    pass

class Curve(object):
    pass

@overload(Sprite, Point, Direction, int)
def add_bullet(sprite, start, direction, speed):
    # ...

@overload(Sprite, Point, Point, int, int)
def add_bullet(sprite, start, headto, speed, acceleration):
    # ...

@overload(Sprite, str)
def add_bullet(sprite, script):
    # ...

@overload(Sprite, Curve, speed)
def add_bullet(sprite, curve, speed):
    # ...

目前最严格的限制是:

不支持方法,只支持非类成员的函数; 继承没有被处理; 不支持Kwargs; 注册新函数应该在导入时完成,这是不线程安全的

根据定义,在python中重载函数是不可能的(详细信息请阅读下文),但您可以使用简单的装饰器实现类似的功能

class overload:
    def __init__(self, f):
        self.cases = {}

    def args(self, *args):
        def store_function(f):
            self.cases[tuple(args)] = f
            return self
        return store_function

    def __call__(self, *args):
        function = self.cases[tuple(type(arg) for arg in args)]
        return function(*args)

你可以这样用

@overload
def f():
    pass

@f.args(int, int)
def f(x, y):
    print('two integers')

@f.args(float)
def f(x):
    print('one float')


f(5.5)
f(1, 2)

修改它以适应您的用例。

概念的澄清

function dispatch: there are multiple functions with the same name. Which one should be called? two strategies static/compile-time dispatch (aka. "overloading"). decide which function to call based on the compile-time type of the arguments. In all dynamic languages, there is no compile-time type, so overloading is impossible by definition dynamic/run-time dispatch: decide which function to call based on the runtime type of the arguments. This is what all OOP languages do: multiple classes have the same methods, and the language decides which one to call based on the type of self/this argument. However, most languages only do it for the this argument only. The above decorator extends the idea to multiple parameters.

为了澄清这一点,假设我们用一种假想的静态语言定义函数

void f(Integer x):
    print('integer called')

void f(Float x):
    print('float called')

void f(Number x):
    print('number called')


Number x = new Integer('5')
f(x)
x = new Number('3.14')
f(x)

使用静态分派(重载),您将看到“number被调用”两次,因为x已被声明为number,这就是重载所关心的。在动态分派中,你会看到“integer called, float called”,因为它们是函数被调用时x的实际类型。

你可以用下面的Python代码来实现这一点:

@overload
def test(message: str):
    return message

@overload
def test(number: int):
    return number + 1

我认为Bullet类层次结构和相关联的多态性是正确的方法。通过使用元类,可以有效地重载基类构造函数,这样调用基类就会创建适当的子类对象。下面是一些示例代码,以说明我的意思的本质。

更新

代码已被修改为在Python 2和Python 3下运行,以保持相关性。这样做的方式避免了使用Python的显式元类语法,这在两个版本之间是不同的。

为了实现这一目标,在创建Bullet基类时显式调用元类来创建BulletMeta类的BulletMetaBase实例(而不是使用__metaclass__= class属性或根据Python版本使用元类关键字参数)。

class BulletMeta(type):
    def __new__(cls, classname, bases, classdict):
        """ Create Bullet class or a subclass of it. """
        classobj = type.__new__(cls, classname, bases, classdict)
        if classname != 'BulletMetaBase':
            if classname == 'Bullet':  # Base class definition?
                classobj.registry = {}  # Initialize subclass registry.
            else:
                try:
                    alias = classdict['alias']
                except KeyError:
                    raise TypeError("Bullet subclass %s has no 'alias'" %
                                    classname)
                if alias in Bullet.registry: # unique?
                    raise TypeError("Bullet subclass %s's alias attribute "
                                    "%r already in use" % (classname, alias))
                # Register subclass under the specified alias.
                classobj.registry[alias] = classobj

        return classobj

    def __call__(cls, alias, *args, **kwargs):
        """ Bullet subclasses instance factory.

            Subclasses should only be instantiated by calls to the base
            class with their subclass' alias as the first arg.
        """
        if cls != Bullet:
            raise TypeError("Bullet subclass %r objects should not to "
                            "be explicitly constructed." % cls.__name__)
        elif alias not in cls.registry: # Bullet subclass?
            raise NotImplementedError("Unknown Bullet subclass %r" %
                                      str(alias))
        # Create designated subclass object (call its __init__ method).
        subclass = cls.registry[alias]
        return type.__call__(subclass, *args, **kwargs)


class Bullet(BulletMeta('BulletMetaBase', (object,), {})):
    # Presumably you'd define some abstract methods that all here
    # that would be supported by all subclasses.
    # These definitions could just raise NotImplementedError() or
    # implement the functionality is some sub-optimal generic way.
    # For example:
    def fire(self, *args, **kwargs):
        raise NotImplementedError(self.__class__.__name__ + ".fire() method")

    # Abstract base class's __init__ should never be called.
    # If subclasses need to call super class's __init__() for some
    # reason then it would need to be implemented.
    def __init__(self, *args, **kwargs):
        raise NotImplementedError("Bullet is an abstract base class")


# Subclass definitions.
class Bullet1(Bullet):
    alias = 'B1'
    def __init__(self, sprite, start, direction, speed):
        print('creating %s object' % self.__class__.__name__)
    def fire(self, trajectory):
        print('Bullet1 object fired with %s trajectory' % trajectory)


class Bullet2(Bullet):
    alias = 'B2'
    def __init__(self, sprite, start, headto, spead, acceleration):
        print('creating %s object' % self.__class__.__name__)


class Bullet3(Bullet):
    alias = 'B3'
    def __init__(self, sprite, script): # script controlled bullets
        print('creating %s object' % self.__class__.__name__)


class Bullet4(Bullet):
    alias = 'B4'
    def __init__(self, sprite, curve, speed): # for bullets with curved paths
        print('creating %s object' % self.__class__.__name__)


class Sprite: pass
class Curve: pass

b1 = Bullet('B1', Sprite(), (10,20,30), 90, 600)
b2 = Bullet('B2', Sprite(), (-30,17,94), (1,-1,-1), 600, 10)
b3 = Bullet('B3', Sprite(), 'bullet42.script')
b4 = Bullet('B4', Sprite(), Curve(), 720)
b1.fire('uniform gravity')
b2.fire('uniform gravity')

输出:

creating Bullet1 object
creating Bullet2 object
creating Bullet3 object
creating Bullet4 object
Bullet1 object fired with uniform gravity trajectory
Traceback (most recent call last):
  File "python-function-overloading.py", line 93, in <module>
    b2.fire('uniform gravity') # NotImplementedError: Bullet2.fire() method
  File "python-function-overloading.py", line 49, in fire
    raise NotImplementedError(self.__class__.__name__ + ".fire() method")
NotImplementedError: Bullet2.fire() method

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等。