Sobrecarga de función de Python
design method-overloading (12)
Sé que Python no es compatible con la sobrecarga de métodos, pero me he encontrado con un problema que parece que no puedo resolver de una manera Pythonic agradable.
Estoy haciendo un juego donde un personaje necesita disparar una variedad de balas, pero ¿cómo escribo diferentes funciones para crear estas balas? Por ejemplo, supongamos que tengo una función que crea una bala que viaja del punto A al B con una velocidad dada. Escribiría una función como esta:
def add_bullet(sprite, start, headto, speed):
... Code ...
Pero quiero escribir otras funciones para crear viñetas como:
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 ...
Y así sucesivamente con muchas variaciones. ¿Hay una mejor manera de hacerlo sin utilizar tantos argumentos de palabras clave porque se vuelve un poco feo rápido? Renombrar cada función es bastante malo también porque obtienes add_bullet1
, add_bullet2
, o add_bullet_with_really_long_name
.
Para abordar algunas respuestas:
No, no puedo crear una jerarquía de clase Bullet porque es muy lenta. El código real para administrar viñetas está en C y mis funciones son envoltorios alrededor de C API.
Sé acerca de los argumentos de la palabra clave pero es molesto comprobar todo tipo de combinaciones de parámetros, pero los argumentos predeterminados ayudan a asignar
acceleration=0
Creo que su requisito básico es tener una sintaxis similar a C / C ++ en Python con el menor dolor de cabeza posible. Aunque me gustó la respuesta de Alexander Poluektov, no funciona para las clases.
Lo siguiente debería funcionar para las clases. Funciona distinguiendo por el número de argumentos que no son palabras clave (pero no admite distinguir por tipo):
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
Y puede usarse simplemente así:
test = TestOverloading()
test.overloaded_function("I''m a Sprite", 0, "Right")
print
test.overloaded_function("I''m another Sprite", "while x == True: print ''hi''")
Salida:
Esto es una sobrecarga 3
Sprite: soy un Sprite
Inicio: 0
Dirección: derechaEsto es sobrecarga 2
Sprite: soy otro Sprite
Guión:
mientras x == Verdadero: imprimir ''hola''
Creo que una jerarquía de clase Bullet
con el polimorfismo asociado es el camino a seguir. Puede sobrecargar eficazmente el constructor de la clase base utilizando una metaclase para que al llamar a la clase base se genere el objeto de la subclase adecuada. A continuación se muestra un código de muestra para ilustrar la esencia de lo que quiero decir.
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 == ''Bullet'': # base class definition?
classobj.registry = {} # initialize class 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
# instance factory for subclasses
# subclasses should only be instantiated by calls to the base class
# with their subclass''s alias as the first arg
def __call__(cls, alias, *args, **kwargs):
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(object):
__metaclass__ = BulletMeta
# 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__(),
# then then it might 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) # creating Bullet1 object
b2 = Bullet(''B2'', Sprite(), (-30,17,94), (1,-1,-1), 600, 10) # creating Bullet2 object
b3 = Bullet(''B3'', Sprite(), ''bullet42.script'') # creating Bullet3 object
b4 = Bullet(''B4'', Sprite(), Curve(), 720) # creating Bullet4 object
b1.fire(''uniform gravity'') # Bullet1 object fired with uniform gravity trajectory
b2.fire(''uniform gravity'') # NotImplementedError: Bullet2.fire() method
En Python 3.4 se añadió PEP-0443. Funciones genéricas de un solo despacho .
Aquí hay una breve descripción de la API de PEP.
Para definir una función genérica, decorarla con el decorador @singledispatch. Tenga en cuenta que el envío ocurre en el tipo del primer argumento. Crea tu función en consecuencia:
from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
if verbose:
print("Let me just say,", end=" ")
print(arg)
Para agregar implementaciones sobrecargadas a la función, use el atributo register () de la función genérica. Este es un decorador, tomando un parámetro de tipo y decorando una función que implementa la operación para ese tipo:
@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)
Este tipo de comportamiento generalmente se resuelve (en lenguajes OOP) usando Polimorfismo. Cada tipo de bala sería responsable de saber cómo viaja. Por ejemplo:
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..
}
Pase tantos argumentos a la función c_ que exista, luego haga el trabajo de determinar qué función c llamar en función de los valores en la función c inicial. Entonces, python solo debería estar llamando a la función one c. Esa función one c examina los argumentos y luego puede delegar a otras funciones c apropiadamente.
Básicamente, solo utiliza cada subclase como un contenedor de datos diferente, pero al definir todos los argumentos potenciales en la clase base, las subclases pueden ignorar libremente aquellas con las que no hacen nada.
Cuando aparece un nuevo tipo de viñeta, simplemente puede definir una propiedad más en la base, cambiar la función de python para que pase la propiedad adicional, y la función c_f que examina los argumentos y delega apropiadamente. No suena tan mal, supongo.
Lo que está pidiendo se llama despacho múltiple . Vea ejemplos del lenguaje de Julia que demuestran diferentes tipos de despachos.
Sin embargo, antes de analizar eso, primero abordaremos por qué la sobrecarga no es realmente lo que quieres en Python.
¿Por qué no sobrecargar?
Primero, uno necesita comprender el concepto de sobrecarga y por qué no es aplicable a python.
Al trabajar con lenguajes que pueden discriminar tipos de datos en tiempo de compilación, la selección entre las alternativas puede ocurrir en tiempo de compilación. El acto de crear tales funciones alternativas para la selección en tiempo de compilación generalmente se denomina sobrecarga de una función. ( Wikipedia )
Python es un lenguaje de tipado dynamically , por lo que el concepto de sobrecarga simplemente no se aplica a él. Sin embargo, no todo está perdido, ya que podemos crear tales funciones alternativas en tiempo de ejecución:
En los lenguajes de programación que difieren la identificación del tipo de datos hasta el tiempo de ejecución, la selección entre funciones alternativas debe ocurrir en tiempo de ejecución, en función de los tipos de argumentos de funciones determinados dinámicamente. Las funciones cuyas implementaciones alternativas se seleccionan de esta manera se denominan más generalmente como multimétodos . ( Wikipedia )
Entonces deberíamos poder hacer multimétodos en python o, como se llama alternativamente, envío múltiple .
Envío múltiple
Los métodos multimétodos también se denominan despacho múltiple :
Múltiples dispatch o multimethods es la característica de algunos lenguajes de programación orientados a objetos en los que una función o método se puede distribuir de forma dinámica en función del tipo de tiempo de ejecución (dinámico) de más de uno de sus argumentos. ( Wikipedia )
Python no es compatible con esto de fábrica 1 . Pero, como sucede, hay un excelente paquete de Python llamado multipledispatch que hace exactamente eso.
Solución
A continuación, le mostramos cómo podemos usar el paquete multipledispatch 2 para implementar sus métodos:
>>> 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 actualmente es compatible con el docs.python.org/3/library/…
2. Tenga cuidado de no usar multipledispatch en un entorno de subprocesos múltiples, o obtendrá un comportamiento extraño.
Puede usar la solución "roll-your-own" para la sobrecarga de funciones. Éste es copiado del artículo de Guido van Rossum sobre multimétodos (porque hay poca diferencia entre mm y sobrecarga en 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
El uso sería
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):
# ...
Las limitaciones más restrictivas en este momento son:
- los métodos no son compatibles, solo las funciones que no son miembros de la clase;
- la herencia no se maneja;
- kwargs no son compatibles;
- El registro de nuevas funciones debe hacerse en el momento de la importación. Lo que no es seguro para subprocesos
Python admite "sobrecarga de método" a medida que lo presentas. De hecho, lo que acabas de describir es trivial de implementar en Python, de muchas maneras diferentes, pero yo iría con:
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
En el código anterior, el default
es un valor predeterminado plausible para esos argumentos, o None
. A continuación, puede llamar al método con solo los argumentos que le interesen y Python utilizará los valores predeterminados.
También podrías hacer algo como esto:
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
Otra alternativa es enlazar directamente la función deseada directamente a la clase o instancia:
def some_implementation(self, arg1, arg2, arg3):
# implementation
my_class.add_bullet = some_implementation_of_add_bullet
Otra forma es usar un patrón abstracto de fábrica:
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
Una posible opción es usar el módulo de parchereducido como se detalla aquí: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch
En lugar de hacer esto:
def add(self, other):
if isinstance(other, Foo):
...
elif isinstance(other, Bar):
...
else:
raise NotImplementedError()
Puedes hacerlo:
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)
Con el uso resultante:
>>> add(1, 2)
3
>>> add(1, ''hello'')
''1 + hello''
Use argumentos de palabra clave con valores predeterminados. P.ej
def add_bullet(sprite, start=default, direction=default, script=default, speed=default):
En el caso de una bala recta versus una bala curva, agregaría dos funciones: add_bullet_straight
y add_bullet_curved
.
Utilice múltiples argumentos de palabras clave en la definición o cree una jerarquía de Bullet
cuyas instancias se pasen a la función.
métodos de sobrecarga es complicado en Python. Sin embargo, podría haber un uso de pasar el dict, la lista o las variables primitivas.
He intentado algo para mis casos de uso, esto podría ayudar aquí a entender a las personas sobrecargar los métodos.
Tomemos su ejemplo:
un método de sobrecarga de clase con llamar a los métodos de diferentes clases.
def add_bullet(sprite=None, start=None, headto=None, spead=None, acceleration=None):
pasar los argumentos de la clase remota:
add_bullet(sprite = ''test'', start=Yes,headto={''lat'':10.6666,''long'':10.6666},accelaration=10.6}
O
add_bullet(sprite = ''test'', start=Yes, headto={''lat'':10.6666,''long'':10.6666},speed=[''10'',''20,''30'']}
Por lo tanto, se está manejando la lista, el diccionario o las variables primitivas de la sobrecarga de métodos.
pruébalo para tus códigos.
Pasando los argumentos de palabra clave .
def add_bullet(**kwargs):
#check for the arguments listed above and do the proper things