Перегрузка функции Python - PullRequest
163 голосов
/ 22 июня 2011

Я знаю, что 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

Ответы [ 13 ]

104 голосов
/ 17 марта 2015

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

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

Почему бы не перегрузить?

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

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

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

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

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

103 голосов
/ 22 июня 2011

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 
83 голосов
/ 05 сентября 2011

Для перегрузки функций вы можете использовать решение по принципу «накатить».Этот скопирован из статьи Гвидо ван Россума о мультиметодах (поскольку разница между 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 не поддерживаются;
  • регистрация новых функций должна выполняться во время импорта, вещь не поточнобезопасна
34 голосов
/ 29 июля 2014

Возможный вариант - использовать модуль 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'
14 голосов
/ 01 января 2016

В 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)
11 голосов
/ 22 июня 2011

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

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

6 голосов
/ 22 июня 2011

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

def add_bullet(**kwargs):
    #check for the arguments listed above and do the proper things
4 голосов
/ 03 октября 2014

Я думаю, что ваше основное требование - иметь синтаксис, подобный 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
Спрайт: я другой Спрайт
Сценарий:
в то время как x == True: выведите 'hi'

3 голосов
/ 29 января 2019

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

3 голосов
/ 22 июня 2011

Я думаю, что иерархия классов 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
...