首页 文章

Python函数重载

提问于
浏览
137

我知道Python不支持方法重载,但我遇到了一个问题,我似乎无法以一种漂亮的Pythonic方式解决这个问题 .

我正在制作一个角色需要射击各种子弹的游戏,但是我如何编写不同的功能来制作这些子弹呢?例如,假设我有一个函数可以创建一个以给定速度从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_bullet1add_bullet2add_bullet_with_really_long_name .

要解决一些问题:

  • 不,我无法创建Bullet类层次结构,因为它太慢了 . 管理项目符号的实际代码是在C中,我的函数是围绕C API的包装器 .

  • 我知道关键字参数但是检查各种参数组合变得很烦人,但默认参数有助于分配 acceleration=0

12 回答

  • 94

    当你提出它时,Python确实支持“方法重载” . 事实上,你刚才描述的内容在Python中以很多不同的方式实现是微不足道的,但我会选择:

    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将使用默认值 .

    你也可以这样做:

    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
    

    另一种方法是直接将所需函数挂接到类或实例:

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

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

    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
    
  • 79

    您要求的是多次调度 . 请参阅Julia语言示例,它们演示了不同类型的调度 .

    但是,在看这个之前,我们首先要解决为什么在python中你不想要重载的原因 .

    为什么不重载?

    首先需要了解重载的概念以及为什么它不适用于python .

    在编译时使用可以区分数据类型的语言时,可以在编译时选择替代方案 . 为编译时选择创建这样的替代函数的行为通常被称为重载函数 . (维基百科)

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

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

    所以我们应该能够在python中执行多方法,或者,或者称为多方式调度 .

    多次派遣

    多方法也称为多次派遣:

    多个调度或多方法是一些面向对象编程语言的特性,其中可以基于多个参数的运行时(动态)类型动态调度函数或方法 . (维基百科)

    Python不支持开箱即用1 . 但是,正如它发生的那样,有一个名为multipledispatch的优秀python包正是如此 .

    解决方案

    以下是我们如何使用multipledispatch 2包来实现您的方法:

    >>> 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,否则会产生奇怪的行为 .

  • 9

    您可以使用"roll-your-own"解决方案进行功能重载 . 这个是从Guido van Rossum's article复制的关于multimethods(因为在python中mm和重载之间几乎没有区别):

    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不受支持;

    • 注册新函数应该在导入时完成,事情不是线程安全的

  • 12

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

    在Python 3.4中添加了PEP-0443. Single-dispatch generic functions .

    以下是PEP的简短API描述 .

    要定义泛型函数,请使用@singledispatch装饰器进行装饰 . 请注意,调度发生在第一个参数的类型上 . 相应地创建您的功能:

    from functools import singledispatch
    @singledispatch
    def fun(arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")
        print(arg)
    

    要向函数添加重载实现,请使用泛型函数的register()属性 . 这是一个装饰器,它接受一个类型参数并装饰一个实现该操作的函数对于那种类型:

    @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)
    
  • 3

    通常使用多态性来解决这种类型的行为(在OOP语言中) . 每种类型的子弹都有责任了解它的行进方式 . 例如:

    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_function,然后根据初始c函数中的值确定要调用哪个c函数 . 所以,python应该只调用一个c函数 . 一个c函数查看参数,然后可以适当地委托给其他c函数 .

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

    当一种新类型的子弹出现时,您可以简单地在基础上定义一个属性,更改一个python函数以便它传递额外属性,以及一个c_function来检查参数和委托 . 我觉得听起来不太糟糕 .

  • 1

    passing keyword args .

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

    我认为你的基本要求是在python中使用类似C / C的语法,并且最不可能 . 虽然我喜欢Alexander Poluektov的答案,但它不适用于课程 .

    以下应适用于课程 . 它的工作原理是区分非关键字参数的数量(但不支持按类型区分):

    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
    

    它可以像这样使用:

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

    输出:

    这是重载3 Sprite:我是一个Sprite Start:0 Direction:Right这是重载2 Sprite:我是另一个Sprite脚本:x == True:打印'hi'

  • 2

    在定义中使用多个关键字参数,或者创建一个实例传递给函数的 Bullet 层次结构 .

  • 6

    我认为具有相关多态性的 Bullet 类层次结构是可行的方法 . 您可以通过使用元类有效地重载基类构造函数,以便调用基类导致创建适当的子类对象 . 下面是一些示例代码来说明我的意思的本质 .

    Updated

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

    为了实现该目标, BulletMeta 类的 BulletMetaBase 实例是通过在创建 Bullet 基类时显式调用元类而创建的(而不是使用 __metaclass__= 类属性或通过 metaclass 关键字参数,具体取决于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
    
  • 29

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

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

    在直子弹与弯曲子弹的情况下,我将添加两个函数: add_bullet_straightadd_bullet_curved .

  • 82

    在python中重载方法很棘手 . 但是,可以使用传递dict,list或原始变量 .

    我已经为我的用例尝试了一些东西,这可以帮助理解人们重载方法 .

    我们举个例子:

    一个类重载方法,调用来自不同类的方法 .

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

    从远程类传递参数:

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

    要么

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

    因此,正在从方法重载实现列表,字典或原始变量的处理 .

    试试你的代码吧 .

相关问题