Перегрузка функций 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_bullet1
, add_bullet2
, либо add_bullet_with_really_long_name
.
Чтобы ответить на некоторые вопросы:
Нет, я не могу создать иерархию маркерных классов, потому что это слишком медленно. Фактический код для управления маркерами написан на C, а мои функции являются оболочками вокруг C API.
Я знаю о аргументах ключевого слова, но проверка всевозможных комбинаций параметров начинает раздражать, но аргументы по умолчанию помогают выделить такие
acceleration=0
Переведено автоматически
Ответ 1
То, что вы запрашиваете, называется множественной отправкой. Смотрите примеры языка Julia, которые демонстрируют различные типы отправок.
Однако, прежде чем рассматривать это, мы сначала разберемся, почему перегрузка на самом деле не то, что вы хотите в Python.
Почему не перегрузка?
Во-первых, нужно понять концепцию перегрузки и почему она неприменима к Python.
При работе с языками, которые могут различать типы данных во время компиляции, выбор между альтернативами может происходить во время компиляции. Процесс создания таких альтернативных функций для выбора во время компиляции обычно называется перегрузкой функции. (Википедия)
Python - это динамически типизированный язык, поэтому концепция перегрузки к нему просто неприменима. Однако не все потеряно, поскольку мы можем создавать такие альтернативные функции во время выполнения:
В языках программирования, которые откладывают идентификацию типа данных до времени выполнения, выбор между альтернативными функциями должен происходить во время выполнения на основе динамически определяемых типов аргументов функций. Функции, альтернативные реализации которых выбираются таким образом, чаще всего называются мультиметодами. (Википедия)
Итак, мы должны уметь выполнять мультиметоды на Python — или, как это альтернативно называется: множественная отправка.
Множественная отправка
Мультиметоды также называются множественной отправкой:
Множественная отправка или мультиметоды - это особенность некоторых объектно-ориентированных языков программирования, в которых функция или метод могут быть динамически отправлены на основе типа времени выполнения (dynamic) более чем одного из их аргументов. (Википедия)
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**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
- В настоящее время Python 3 поддерживает одиночную отправку
- Будьте осторожны, чтобы не использовать 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
Еще один способ - использовать абстрактный фабричный шаблон:
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
Вы можете использовать "собственное решение" для перегрузки функций. Это скопировано из статьи Гвидо ван Россума о мультиметодах (потому что разница между мультиметодами и перегрузкой в 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
The usage would be
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'