Python函数重载

我知道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_bullet1add_bullet2,就是add_bullet_with_really_long_name

以下是一些问题的答案:

  1. 不,我不能创建一个子弹类层次结构,因为那太慢了。管理项目符号的实际代码是用C编写的,我的函数是围绕C API的包装器。

  2. 我知道关键字参数,但检查各种形参组合是很烦人的,但默认参数帮助分配,如acceleration=0

328577 次浏览

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

在默认值中使用关键字参数。如。

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

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

传递关键字args

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

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

这种类型的行为通常(在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,以适当地检查参数和委托。我想听起来还不算太糟。

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

更新

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

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

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

你可以用“自己卷”;函数重载的解决方案。这个是从关于多方法的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;
  • 注册新函数应该在导入时完成,这是不线程安全的
一个可能的选项是使用multipledispatch模块,详细如下: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch < / p >

不要这样做:

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'

我认为你的基本要求是在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'")

输出:

This is overload 3
精灵:我是精灵
开始:0
方向:右

This is overload 2
精灵:我是另一个精灵
脚本:
当x == True: print 'hi'

你要求的是多分派。参见茱莉亚语言示例,其中演示了不同类型的分派。

然而,在研究它之前,我们将首先解决为什么重载在Python中不是你真正想要的。

为什么不超载?

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

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

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

在将数据类型标识延迟到 运行时在备选项中进行选择 函数必须在运行时根据动态确定的值发生 函数参数的类型。其替代函数 以这种方式选择的实现引用最多 通常为多重方法。(维基百科) < / p >

因此,我们应该能够在python中执行多重方法——或者,它也可以称为:多分派

多分派

多方法也被称为多分派:

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

Python不支持开箱即用的__abc0,但是,恰好有一个名为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,否则你会得到奇怪的行为。 < / sub > < / p >

在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)

若要向函数添加重载实现,请使用泛型函数的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)

在Python中重载方法是很棘手的。但是,可以使用传递字典、列表或原始变量的方法。

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

让我们以你为例:

类重载方法调用不同类的方法。

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']}

因此,从方法重载中实现对列表、字典或基本变量的处理。

为您的代码尝试一下。

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

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

参见:类型提示PEP 484

在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)

修改它以适应您的用例。

概念的澄清

  • 调度函数:有多个同名函数。应该叫哪一个?两种策略
  • 静态/编译时调度 (又名。“overloading")。根据参数的编译时类型决定调用哪个函数。在所有动态语言中,没有编译时类型,因此根据定义重载是不可能的
  • 动态/运行时调度:根据实参的运行时类型决定调用哪个函数。这就是所有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)

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

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必须是最外层的装饰器。这是 带有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=}")

输出:

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

相同的图案可以用于其他类似的装饰:

. Staticmethod, abstractmethod和其他方法

你可以很容易地在Python中实现函数重载。下面是一个使用floatsintegers的例子:

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

代码背后的主要思想是,一个类包含你想要实现的不同(重载)函数,而Dictionary作为router工作,根据输入type(x)将代码指向正确的函数。

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

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

李子以一种简单的python方式支持它。从下面的README复制一个示例。

from plum import dispatch


@dispatch
def f(x: str):
return "This is a string!"
    



@dispatch
def f(x: int):
return "This is an integer!"


>>> f("1")
'This is a string!'


>>> f(1)
'This is an integer!'

您还可以尝试这段代码。我们可以尝试任何论点

# Finding the average of given number of arguments
def avg(*args):   # args is the argument name we give
sum = 0
for i in args:
sum += i
average = sum/len(args)   # Will find length of arguments we given
print("Avg: ", average)


# call function with different number of arguments
avg(1,2)
avg(5,6,4,7)
avg(11,23,54,111,76)

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

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


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