Python函数重载

Python function overloading

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

我正在制作一个游戏,一个角色需要发射各种子弹,但我如何编写不同的功能来创建这些子弹?例如,假设我有一个函数,可以创建一个子弹,以给定的速度从A点移动到B点。我会写一个这样的函数:

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

但我想编写其他用于创建项目符号的函数,如:

1
2
3
4
5
    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_bullet1add_bullet2add_bullet_with_really_long_name

要解决一些问题:

  • 不,我不能创建项目符号类层次结构,因为这太慢了。管理项目符号的实际代码是C语言,而我的函数是围绕C API的包装器。

  • 我知道关键字参数,但检查各种参数组合会让人恼火,但默认参数有助于分配,如acceleration=0


  • python确实支持"方法重载"。事实上,您刚才描述的在Python中以许多不同的方式实现是很简单的,但是我将使用:

    1
    2
    3
    4
    5
    6
    7
    class Character(object):
        # your character __init__ and other methods go here

        def add_bullet(self, sprite=default, start=default,
                     direction=default, speed=default, accel=default,
                      curve=default):
            # do stuff with your arguments

    在上述代码中,default是这些参数的一个合理的默认值,或None。然后,您可以只使用感兴趣的参数调用该方法,而python将使用默认值。

    你也可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Character(object):
        # your character __init__ and other methods go here

        def add_bullet(self, **kwargs):
            # here you can unpack kwargs as (key, values) and
            # do stuff with them, and use some global dictionary
            # to provide default values and ensure that ``key``
            # is a valid argument...

            # do stuff with your arguments

    另一种选择是直接将所需函数挂钩到类或实例:

    1
    2
    3
    def some_implementation(self, arg1, arg2, arg3):
      # implementation
    my_class.add_bullet = some_implementation_of_add_bullet

    另一种方法是使用抽象的工厂模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Character(object):
       def __init__(self, bfactory, *args, **kwargs):
           self.bfactory = bfactory
       def add_bullet(self):
           sprite = self.bfactory.sprite()
           speed = self.bfactory.speed()
           # do stuff with your sprite and speed

    class pretty_and_fast_factory(object):
        def sprite(self):
           return pretty_sprite
        def speed(self):
           return 10000000000.0

    my_character = Character(pretty_and_fast_factory(), a1, a2, kw1=v1, kw2=v2)
    my_character.add_bullet() # uses pretty_and_fast_factory

    # now, if you have another factory called"ugly_and_slow_factory"
    # you can change it at runtime in python by issuing
    my_character.bfactory = ugly_and_slow_factory()

    # In the last example you can see abstract factory and"method
    # overloading" (as you call it) in action


    你所要求的,叫做多重调度。请参阅Julia语言示例,其中演示了不同类型的分派。

    然而,在研究这个问题之前,我们将首先讨论一下为什么重载并不是您在Python中真正想要的。

    为什么不超载?

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

    When working with languages that can discriminate data types at
    compile-time, selecting among the alternatives can occur at
    compile-time. The act of creating such alternative functions for
    compile-time selection is usually referred to as overloading a
    function. (Wikipedia)

    Python是一种动态类型语言,因此重载的概念不适用于它。但是,所有这些都不会丢失,因为我们可以在运行时创建这样的可选函数:

    In programming languages that defer data type identification until
    run-time the selection among alternative
    functions must occur at run-time, based on the dynamically determined
    types of function arguments. Functions whose alternative
    implementations are selected in this manner are referred to most
    generally as multimethods. (Wikipedia)

    因此,我们应该能够在Python中执行多方法,或者,正如它被称为多调度一样。

    多次调度

    多方法也称为多调度:

    Multiple dispatch or multimethods is the feature of some
    object-oriented programming languages in which a function or method
    can be dynamically dispatched based on the run time (dynamic) type of
    more than one of its arguments. (Wikipedia)

    python在box1之外不支持这个功能。但是,碰巧有一个名为multipledispatch的优秀python包可以做到这一点。

    解决方案

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    >>> 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
    >>> 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,否则会出现奇怪的行为。


    您可以使用"滚动您自己的"解决方案进行函数重载。这是从guido van rossum关于多方法的文章中复制的(因为mm和python中的重载几乎没有区别):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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

    使用方法是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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;
    • 注册新函数应该在导入时完成,这不是线程安全的。


    一种可能的选择是使用multipleedispatch模块,如下所述:http://matthewrocklin.com/blog/work/2014/02/25/multiple-dispatch

    而不是这样做:

    1
    2
    3
    4
    5
    6
    7
    def add(self, other):
        if isinstance(other, Foo):
            ...
        elif isinstance(other, Bar):
            ...
        else:
            raise NotImplementedError()

    您可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    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)

    使用结果:

    1
    2
    3
    4
    5
    >>> add(1, 2)
    3

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


    在python中添加了pep-0443。单调度通用函数。

    以下是PEP的简短API描述。

    要定义一个通用函数,请用@singledispatch decorator来修饰它。注意,调度发生在第一个参数的类型上。相应地创建您的函数:

    1
    2
    3
    4
    5
    6
    from functools import singledispatch
    @singledispatch
    def fun(arg, verbose=False):
        if verbose:
            print("Let me just say,", end="")
        print(arg)

    要向函数添加重载实现,请使用泛型函数的register()属性。这是一个修饰器,获取类型参数并修饰实现该类型操作的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @fun.register(int)
    def _(arg, verbose=False):
        if verbose:
            print("Strength in numbers, eh?", end="")
        print(arg)

    @fun.register(list)
    def _(arg, verbose=False):
        if verbose:
            print("Enumerate this:")
        for i, elem in enumerate(arg):
            print(i, elem)

    这种类型的行为通常使用多态性来解决(在OOP语言中)。每种类型的子弹都有责任知道它是如何运动的。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class Bullet(object):
        def __init__(self):
            self.curve = None
            self.speed = None
            self.acceleration = None
            self.sprite_image = None

    class RegularBullet(Bullet):
        def __init__(self):
            super(RegularBullet, self).__init__()
            self.speed = 10

    class Grenade(Bullet):
        def __init__(self):
            super(Grenade, self).__init__()
            self.speed = 4
            self.curve = 3.5

    add_bullet(Grendade())

    def add_bullet(bullet):
        c_function(bullet.speed, bullet.curve, bullet.acceleration, bullet.sprite, bullet.x, bullet.y)


    void c_function(double speed, double curve, double accel, char[] sprite, ...) {
        if (speed != null && ...) regular_bullet(...)
        else if (...) curved_bullet(...)
        //..etc..
    }

    将尽可能多的参数传递给存在的c_函数,然后根据初始c函数中的值确定要调用的c函数。所以,Python应该只调用一个C函数。一个C函数查看参数,然后可以适当地委托给其他C函数。

    本质上,您只是将每个子类用作不同的数据容器,但是通过在基类上定义所有可能的参数,子类可以自由地忽略它们不做任何事情的参数。

    当出现一种新类型的项目符号时,您只需在基础上再定义一个属性,更改一个python函数,使其传递额外的属性,以及一个检查参数和适当委托的c_函数。我想听起来不错。


    通过传递关键字参数。

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


    我认为您的基本要求是在Python中有一个C/C++类似的语法,但最不头痛。虽然我喜欢亚历山大波卢埃克托夫的答案,但它不适合上课。

    以下内容适用于课堂。它通过区分非关键字参数的数量来工作(但不支持按类型区分):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class TestOverloading(object):
        def overloaded_function(self, *args, **kwargs):
            # Call the function that has the same number of non-keyword arguments.  
            getattr(self,"_overloaded_function_impl_" + str(len(args)))(*args, **kwargs)

        def _overloaded_function_impl_3(self, sprite, start, direction, **kwargs):
            print"This is overload 3"
            print"Sprite: %s" % str(sprite)
            print"Start: %s" % str(start)
            print"Direction: %s" % str(direction)

        def _overloaded_function_impl_2(self, sprite, script):
            print"This is overload 2"
            print"Sprite: %s" % str(sprite)
            print"Script:"
            print script

    它可以这样简单地使用:

    1
    2
    3
    4
    5
    test = TestOverloading()

    test.overloaded_function("I'm a Sprite", 0,"Right")
    print
    test.overloaded_function("I'm another Sprite","while x == True: print 'hi'")

    输出:

    This is overload 3
    Sprite: I'm a Sprite
    Start: 0
    Direction: Right

    This is overload 2
    Sprite: I'm another Sprite
    Script:
    while x == True: print 'hi'


    @overload装饰器添加了类型提示(pep 484)。虽然这不会改变python的行为,但它确实使理解正在发生的事情变得更容易,并且使mypy能够检测错误。参见:类型提示和PEP 484


    我认为一个具有相关多态性的Bullet类层次结构是可行的。通过使用元类,可以有效地重载基类构造函数,这样调用基类就可以创建适当的子类对象。下面是一些示例代码,用于说明我所指内容的本质。

    更新

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

    为了实现这一目标,创建BulletMeta类时,通过显式调用元类(而不是使用__metaclass__=类属性或通过依赖于python版本的metaclass关键字参数)创建BulletMetaBase实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    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')

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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


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


    在Python中,重载方法是很棘手的。但是,也可以使用传递dict、list或基元变量。

    我已经为我的用例做了一些尝试,这有助于理解人们超载方法。

    让我们举个例子:

    具有从不同类调用方法的类重载方法。

    1
    def add_bullet(sprite=None, start=None, headto=None, spead=None, acceleration=None):

    从远程类传递参数:

    1
    add_bullet(sprite = 'test', start=Yes,headto={'lat':10.6666,'long':10.6666},accelaration=10.6}

    1
    add_bullet(sprite = 'test', start=Yes, headto={'lat':10.6666,'long':10.6666},speed=['10','20,'30']}

    因此,正在通过方法重载对列表、字典或基元变量进行处理。

    试试看你的密码。


    使用带默认值的关键字参数。例如。

    1
    def add_bullet(sprite, start=default, direction=default, script=default, speed=default):

    对于直弹头和弯曲弹头,我增加了两个功能:add_bullet_straightadd_bullet_curved