Python函数重载

Bul*_*ets 175 python overloading

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

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

    def add_bullet(sprite, start, headto, speed):
        ... Code ...
Run Code Online (Sandbox Code Playgroud)

但我想写其他功能来创建子弹,如:

    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 ...
Run Code Online (Sandbox Code Playgroud)

等等有很多变化.有没有更好的方法来做到这一点,而不使用这么多的关键字参数导致它快速变得有点难看.重命名每个功能,因为你要么是非常糟糕过add_bullet1,add_bullet2add_bullet_with_really_long_name.

要解决一些问题:

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

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

And*_*yuk 116

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

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

为什么不超载?

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

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

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

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

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

多次派遣

多方法也称为多次发送:

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

Python不支持开箱即用1.但是,正如它所发生的那样,有一个很好的python包叫做multipledispatch,就是这样做的.

以下是我们如何使用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
Run Code Online (Sandbox Code Playgroud)

1. Python 3目前支持单一调度

2.注意不要在多线程环境中使用 multipledispatch,否则会产生奇怪的行为.

  • @danzeer这不是线程安全的.我看到该参数被两个不同的线程修改(即当另一个线程设置它自己的`speed`值时,`speed`的值可能会在函数中间发生变化)!我花了很长时间才意识到这是图书馆的罪魁祸首. (5认同)
  • 在多线程环境中“ multipledispatch”有什么问题?由于服务器端的代码通常在多线程环境中!只是想把它挖出来! (3认同)
  • 与 single_dispatch 相比,multipledispatch 的优点是它也可以与 python<3.8 中的类方法一起使用。 (2认同)

Esc*_*alo 105

当你提出它时,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
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,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
Run Code Online (Sandbox Code Playgroud)

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

def some_implementation(self, arg1, arg2, arg3):
  # implementation
my_class.add_bullet = some_implementation_of_add_bullet
Run Code Online (Sandbox Code Playgroud)

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

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 
Run Code Online (Sandbox Code Playgroud)

  • 所有这些都看作是变量参数的例子,而不是重载.由于重载允许您具有相同的功能,因为不同类型的参数.例如:sum(real_num1,real_num2)和sum(imaginary_num1,imaginary_num2)两者都有相同的调用语法,但实际上期望2种不同类型作为输入,并且实现也必须在内部更改 (98认同)
  • 使用您将要使用的答案,您将如何向调用者呈现哪些参数一起有意义?只使用一组带有默认值的参数可以提供相同的功能,但就API而言,它更不优雅 (16认同)
  • 以上不是重载,实现将必须检查参数的所有组合输入(或忽略参数),如:`如果精灵和脚本而不是启动而不是方向而不是速度......`只是知道它在一个具体行动.因为调用者可以调用提供所有可用参数的函数.重载时为您定义相关参数的确切集合. (5认同)
  • 当人们说python支持方法重载时,这非常令人沮丧。它不是。在引号中加上“方法重载”这一事实表明您已经意识到了这一事实。您可以通过多种技术获得类似的功能,例如此处提到的一种。但是方法重载有一个非常具体的定义。 (4认同)
  • 我认为的目的是虽然方法重载不是python的一个特性,但可以使用上述机制来达到等效的效果。 (2认同)

Ale*_*tov 87

您可以使用"自己动手"解决方案进行功能重载.这个是从Guido van Rossum关于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
Run Code Online (Sandbox Code Playgroud)

用法是

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):
    # ...
Run Code Online (Sandbox Code Playgroud)

最严格的限制,目前主要有:

  • 不支持方法,只支持非类成员的函数;
  • 继承不处理;
  • 不支持kwargs;
  • 注册新函数应该在导入时完成,事情不是线程安全的

  • 用于在此用例中扩展语言的装饰器的+1. (6认同)
  • +1 因为这是一个好主意(可能还有 OP 应该采用什么)---我从未见过 Python 中的多方法实现。 (3认同)
  • 这与多重调度库相比如何? (2认同)

Dav*_*e C 35

一个可能的选择是使用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()
Run Code Online (Sandbox Code Playgroud)

你可以这样做:

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)
Run Code Online (Sandbox Code Playgroud)

由此产生的用法:

>>> add(1, 2)
3

>>> add(1, 'hello')
'1 + hello'
Run Code Online (Sandbox Code Playgroud)

  • 为什么这不会获得更多选票?我猜是因为缺少示例......我已经创建了一个答案,举例说明如何用*multipledispatch*包实现OP问题的解决方案. (4认同)

Inf*_*ion 16

在Python 3.4中添加了PEP-0443.单调度泛型函数.

以下是PEP的简短API描述.

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

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

要向函数添加重载实现,请使用泛型函数的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)
Run Code Online (Sandbox Code Playgroud)


Jos*_*ton 11

通常使用多态性来解决这种类型的行为(在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..
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


blu*_*ote 10

根据定义,在 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)
Run Code Online (Sandbox Code Playgroud)

你可以像这样使用它

@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)
Run Code Online (Sandbox Code Playgroud)

修改它以适应您的用例。

概念的澄清

  • 函数调度:有多个同名的函数。应该叫哪一个?两种策略
  • 静态/编译时调度又名“重载”)。根据参数的编译时类型决定调用哪个函数。在所有动态语言中,都没有编译时类型,因此根据定义,重载是不可能的
  • 动态/运行时调度:根据参数的运行时类型决定调用哪个函数。这就是所有 OOP 语言所做的:多个类具有相同的方法,语言根据self/this参数的类型决定调用哪一个。但是,大多数语言仅针对this参数执行此操作。上面的装饰器将这个想法扩展到多个参数。

为了清楚起见,假设我们用一种假设的静态语言定义了函数

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)
Run Code Online (Sandbox Code Playgroud)

使用静态调度(重载),您将看到“被调用的数字”两次,因为x已被声明为Number,而这就是重载所关心的。使用动态分派,您将看到“整数调用,浮点调用”,因为这些x是调用函数时的实际类型。


Vla*_*den 9

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=}")
Run Code Online (Sandbox Code Playgroud)

输出

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a
Run Code Online (Sandbox Code Playgroud)

@singledispatchmethod 支持与其他装饰器(例如 @classmethod)嵌套。请注意,为了允许dispatcher.register,singledispatchmethod必须是最外层的装饰器。这是 Negator 类,其 neg 方法是类绑定的:

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=}")
Run Code Online (Sandbox Code Playgroud)

输出:

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a
Run Code Online (Sandbox Code Playgroud)

相同的模式可用于其他类似的装饰器:staticmethod、abstractmethod 等。


Ric*_*ead 8

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

虽然这不会改变 Python 的行为,但它确实让人们更容易理解正在发生的事情,并且让 mypy 检测错误。

请参阅:输入提示PEP 484

  • 你能添加一些例子吗? (5认同)
  • 我同意这里有一个例子会很好,因为这是很好的语法糖,而不是在单独的函数中分离逻辑。这是 mypy 页面上工作的更好详细信息:https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading (3认同)

Nic*_*ord 6

通过传递关键字args.

def add_bullet(**kwargs):
    #check for the arguments listed above and do the proper things
Run Code Online (Sandbox Code Playgroud)

  • @PeterMortensen 我不这么认为,因为 _kwarg_ 是 _keyword argument_ 的缩写。 (4认同)

Tim*_*ski 6

我认为您的基本要求是在 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
Run Code Online (Sandbox Code Playgroud)

它可以像这样简单地使用:

test = TestOverloading()

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

输出:

这是重载 3
精灵:我是精灵
开始:0
方向:右

这是重载 2
Sprite: I'm another Sprite
Script:
while x == True: print 'hi'


小智 6

您可以使用以下 Python 代码来实现此目的:

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

@overload
def test(number: int):
    return number + 1
Run Code Online (Sandbox Code Playgroud)

  • 需要补充的是,您引用的是重载的 pip 包,并且需要先导入装饰器 (7认同)

C-3*_*3PO 5

您可以轻松地在Python中实现函数重载。floats这是使用和 的示例integers

class OverloadedFunction:
    def __init__(self):
        self.router = {int : self.f_int   ,
                       float: self.f_float}
    
    def __call__(self, x):
        return self.router[type(x)](x)
    
    def f_int(self, x):
        print('Integer Function')
        return x**2
    
    def f_float(self, x):
        print('Float Function (Overloaded)')
        return x**3

# f is our overloaded function
f = OverloadedFunction()

print(f(3 ))
print(f(3.))

# Output:
# Integer Function
# 9
# Float Function (Overloaded)
# 27.0
Run Code Online (Sandbox Code Playgroud)

代码背后的主要思想是,一个类包含您想要实现的不同(重载)函数,而字典则充当router,根据输入将您的代码定向到正确的函数type(x)

PS1。对于自定义类,例如Bullet1,您可以按照类似的模式初始化内部字典,例如self.D = {Bullet1: self.f_Bullet1, ...}。其余代码是相同的。

PS2。所提出的解决方案的时间/空间复杂度也很好,O(1)每个操作的平均成本为。


Ign*_*ams 4

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