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

Я знаю, что 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_bullet1, add_bullet2, либо add_bullet_with_really_long_name.

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

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

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

Ответ 1

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

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

Почему не перегрузка?

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

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

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

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

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

Несколько отправки

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

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

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

<суб > 1. Python 3 в настоящее время поддерживает разовую отправку Суб >

<суб > 2. Старайтесь не использовать multipledispatch в многопоточной среде, иначе вы получите странное поведение. Суб >

Ответ 2

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

Еще один способ - использовать абстрактный шаблон factory:

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 

Ответ 3

Вы можете использовать решение "roll-your-own" для перегрузки функций. Этот файл скопирован из статьи 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 не поддерживаются;
  • регистрация новых функций должна выполняться во время импорта, но не в потоковом режиме

Ответ 4

Возможная опция - использовать модуль 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'

Ответ 5

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

Ответ 6

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

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

Ответ 8

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

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

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
  Sprite: Я еще один спрайт   Script:
  while x == True: print 'hi'

Ответ 9

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

Ответ 10

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

обновленный

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

Для достижения этой цели, BulletMetaBase экземпляр BulletMeta класса создаются явным вызов метакласса при создании Bullet BaseClass (а не с помощью __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 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 __init__ should never be called.
    # If subclasses need to call super class __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

Ответ 11

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

Ответ 12

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

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

В случае прямой пули и искривленной пули я бы добавил две функции: add_bullet_straight и add_bullet_curved.

Ответ 13

методы перегрузки сложны в 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']}

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

попробуйте его для своих кодов.

Ответ 14

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

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)

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

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

  • диспетчеризация функции: есть несколько функций с одинаковым именем. Какой из них следует назвать? две стратегии
  • статическая отправка во время компиляции (aka. "перегрузка"). решить, какую функцию вызывать, основываясь на типе аргументов во время компиляции. Во всех динамических языках нет типа времени компиляции, поэтому перегрузка невозможна по определению
  • динамическая/динамическая отправка: решите, какую функцию вызывать, основываясь на типе аргументов во время выполнения. Это то, что делают все языки ООП: несколько классов имеют одинаковые методы, и язык решает, какой из них вызывать, основываясь на типе аргумента self/this. Однако большинство языков делают это только для аргумента this. Приведенный выше декоратор распространяет идею на несколько параметров.

Чтобы разобраться, предположим статический язык и определим функции

void f(Integer x):
    print('number 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 во время вызова функции.