Перегрузка функции Python

Я знаю, что Python не поддерживает перегрузку методов, но я столкнулся с проблемой, которую не могу решить хорошим способом Pythonic.

Я делаю игру, в которой персонажу нужно стрелять различными пулями, но как мне написать разные функции для создания этих пуль? Например, предположим, что у меня есть функция, которая создает пулю, путешествующую из точки А в точку Б с заданной скоростью. Я бы написал такую ​​функцию:

    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,

Чтобы ответить на некоторые ответы:

  1. Нет, я не могу создать иерархию класса Bullet, потому что это слишком медленно. Фактический код для управления маркерами находится на C, а мои функции - обертки вокруг C API.

  2. Я знаю об аргументах ключевого слова, но проверка на все виды комбинаций параметров становится раздражающей, но аргументы по умолчанию помогают выделить как acceleration=0

21 ответ

То, что вы просите, называется многократной отправкой. Смотрите примеры языка Julia, которые демонстрируют различные типы рассылок.

Однако, прежде чем смотреть на это, мы сначала разберемся, почему перегрузка не совсем то, что вам нужно в python.

Почему бы не перегружать?

Сначала нужно понять концепцию перегрузки и почему она не применима к питону.

При работе с языками, которые могут различать типы данных во время компиляции, выбор из альтернатив может происходить во время компиляции. Создание таких альтернативных функций для выбора во время компиляции обычно называется перегрузкой функции. ( Википедия)

Python является динамически типизированным языком, поэтому концепция перегрузки просто к нему не относится. Однако еще не все потеряно, так как мы можем создавать такие альтернативные функции во время выполнения:

В языках программирования, которые откладывают идентификацию типов данных до времени выполнения, выбор среди альтернативных функций должен происходить во время выполнения на основе динамически определенных типов аргументов функции. Функции, чьи альтернативные реализации выбраны таким образом, чаще всего называют мультиметодами. ( Википедия)

Таким образом, мы должны быть в состоянии сделать мультиметоды в python или, как его еще называют, множественную диспетчеризацию.

Многократная отправка

Мультиметоды также называются множественной отправкой:

Множественная диспетчеризация или мультиметоды - это особенность некоторых объектно-ориентированных языков программирования, в которых функция или метод могут динамически отправляться на основе типа времени выполнения (динамического) более чем одного из его аргументов. ( Википедия)

Python не поддерживает это из коробки1. Но, как это бывает, есть отличный пакет python, называемый multipledispatch, который делает именно это.

Решение

Вот как мы можем использовать пакет 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
>>> 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 в многопоточной среде, иначе вы получите странное поведение.

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 

Для перегрузки функций вы можете использовать решение "покатить самостоятельно". Этот скопирован из статьи Гвидо ван Россума о мультиметодах (потому что разница между mm и перегрузкой в ​​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

Вместо этого:

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 3.4 был добавлен PEP-0443. Универсальные функции единой отправки.

Вот краткое описание API от PEP.

Чтобы определить универсальную функцию, украсьте ее с помощью декоратора @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)

@overload Декоратор был добавлен с подсказками типа (PEP 484). Хотя это не меняет поведение python, оно облегчает понимание происходящего и позволяет mypy обнаруживать ошибки.
Смотрите: Тип подсказки и PEP 484

Этот тип поведения обычно решается (на языках ООП) с использованием полиморфизма. Каждый тип пули будет отвечать за знание того, как она движется. Например:

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_, которая проверяет аргументы и делегирует соответственно. Кажется, звучит не так уж плохо.

Просто простой декоратор

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)

Измените его, чтобы адаптировать к вашему варианту использования.

Разъяснение понятий

  • диспетчеризация функций: существует несколько функций с одинаковым именем. Какой из них называть? две стратегии
  • статическая отправка / отправка во время компиляции (такжеизвестная как "перегрузка"). решить, какую функцию вызывать, в зависимости от типа аргументов во время компиляции. Во всех динамических языках нет типа времени компиляции, поэтому перегрузка невозможна по определению
  • динамическая диспетчеризация / диспетчеризация во время выполнения: решить, какую функцию вызывать, в зависимости от типа аргументов во время выполнения. Это то, что делают все языки ООП: несколько классов имеют одни и те же методы, и язык решает, какой из них вызывать, в зависимости от типа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, и это все, о чем заботится перегрузка. При динамической отправке вы увидите "целое число вызвано, число с плавающей запятой вызвано", потому что это фактические типыx в момент вызова функции.

В Python 3.8 добавлен метод functools.singledispatchmethod

Преобразуйте метод в универсальную функцию с однократной отправкой.

Чтобы определить универсальный метод, украсьте его декоратором @singledispatchmethod. Обратите внимание, что отправка происходит по типу первого аргумента non-self или non-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 должен быть самым внешним декоратором. Вот класс 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=}")

Выход:

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

Этот же шаблон можно использовать для других похожих декораторов: staticmethod, abstractmethod и других.

Передав ключевое слово args.

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

Вы можете добиться этого с помощью следующего кода Python:

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

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

Я думаю, что ваше основное требование - иметь синтаксис, подобный C/C++, в python с минимально возможной головной болью. Хотя мне понравился ответ Александра Полуэктова, он не работает на уроках.

Следующее должно работать для классов. Он работает, различая количество аргументов без ключевых слов (но не поддерживает различение по типу):

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
Спрайт: я спрайт
Начало: 0
Направление: Право

Это перегрузка 2
Спрайт: я другой спрайт
Автор сценария:
в то время как х == Истина: выведите "привет"

Вы можете очень легко реализовать перегрузку функций в Python. Вот пример использования floats а также integers:

      class OverloadedFunction:
    def __init__(self):
        self.D = {int: self.f_int, float: self.f_float}
    
    def __call__(self, x):
        return self.D[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

Основная идея кода заключается в том, что класс содержит различные возможные функции, которые вы хотите реализовать, а словарь работает как router, направляя ваш код к нужной функции в зависимости от ввода type(x).

PS1. В случае пользовательских классов, например Bullet1, вы можете инициализировать внутренний словарь по аналогичному шаблону, например self.D = {Bullet1: self.f_Bullet1, ...}. В остальном код такой же.

PS2. Сложность предлагаемого решения во времени / пространстве также довольно хороша, при средней стоимости O(1) за операцию.

Либо используйте несколько аргументов ключевых слов в определении, либо создайте Bullet иерархия, экземпляры которой передаются в функцию.

Я думаю Bullet классовая иерархия с ассоциированным полиморфизмом - путь. Вы можете эффективно перегрузить конструктор базового класса, используя метакласс, так что вызов базового класса приводит к созданию соответствующего объекта подкласса. Ниже приведен пример кода для иллюстрации сути того, что я имею в виду.

обновленный

Код был изменен для запуска под Python 2 и 3, чтобы сохранить его актуальность. Это было сделано таким образом, чтобы избежать использования явного синтаксиса метакласса Python, который варьируется между двумя версиями.

Для достижения этой цели, BulletMetaBase экземпляр BulletMeta класс создается путем явного вызова метакласса при создании 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

Перегрузка методов сложна в Python. Однако может использоваться передача переменных dict, list или primitive.

Я попробовал кое-что для своих вариантов использования, это могло бы помочь здесь понять людей, чтобы перегружать методы.

Давайте возьмем ваш пример:

метод перегрузки класса с вызовом методов из другого класса.

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

Таким образом, достигается обработка для списка, словаря или примитивных переменных из-за перегрузки метода.

попробуйте это для ваших кодов.

Как перегрузить в Python?

Я знаю, что это старый вопрос, но тема по-прежнему очень актуальна, и я не прочитал ни одного четкого и краткого ответа, поэтому решил дать его сам.


Сначала установите пакет:

      pip3 install overloading
pip3 install typing

Затем используйтеoverloadдекоратор из пакета для определения нескольких реализаций функции:

      from typing import Set, List
from overloading import overload

@overload
def process_data(data: List[str], join_string: str) -> str:
    return join_string.join(data)

@overload
def process_data(data: Set[str], join_string: str) -> str:
    return join_string.join(data)

@overload
def process_data(data: List[float], round_to: int) -> List[float]:
    return [round(x, round_to) for x in data]

# And so on ...

Количество и типы аргументов определяют, какая версия функции вызывается.

  • К сожалению, отправка на основе имен аргументов не поддерживается .

Здесь вы можете найти полную документацию.

Эта библиотека поддерживает его простым питоническим способом. Копирование примера из 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!'

Используйте ключевые аргументы со значениями по умолчанию. Например

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

В случае прямой пули по сравнению с изогнутой, я бы добавил две функции: add_bullet_straight а также add_bullet_curved,

Мое решение

      def _either(**kwargs):
    return len([True for _, v in kwargs.items() if v is not None]) == 1

def overload(func, _overloaded_args=None):
    """enable overloading toward all params
    Usage:
        @overload
        def overloaded_func(either1=None, either2=None, either3=None):
            pass
        @overload
        def overloaded_func(must1, must2, either1=None, either2=None):
            pass
    """
    def inner(*func_args, **func_kwargs):
        nonlocal _overloaded_args
        __overloaded_args = _overloaded_args
        if __overloaded_args is None:
            __overloaded_args = list(func_kwargs.keys())
        if __overloaded_args:
            __overloaded_kwargs = {k: v for k, v in func_kwargs.items()
                                  if k in __overloaded_args}
            assert _either(**__overloaded_kwargs), (
                'function overloading should contain a single overloaded param.'
                f' overloaded params: {__overloaded_args}. inputs: {func_kwargs}')
        return func(*func_args, **func_kwargs)
    return inner

def overload_args(*_overloaded_args):
    """enable overloading toward specified params
    Usage:
        @overload_args('either1', 'either2')
        def overloaded_func(either1=None, either2=None):
            pass
        @overload_args('either1', 'either2')
        def overloaded_func(either1=None, either2=None, optional1=None, optional2=123):
            pass
    """
    def inner(func):
        return overload(func, _overloaded_args=_overloaded_args)
    return inner

Вы также можете попробовать этот код. Мы можем попробовать любое количество аргументов

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