Je sais que Python ne prend pas en charge la surcharge de méthodes, mais je suis confronté à un problème que je n'arrive pas à résoudre de manière agréable en Pythonic.
Je suis en train de créer un jeu dans lequel un personnage doit tirer plusieurs balles, mais comment puis-je écrire différentes fonctions pour créer ces balles? Par exemple, supposons que j’ai une fonction qui crée une balle se déplaçant du point A à B avec une vitesse donnée. J'écrirais une fonction comme celle-ci:
def add_bullet(Sprite, start, headto, speed):
... Code ...
Mais je veux écrire d'autres fonctions pour créer des puces comme:
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 ...
Et ainsi de suite avec de nombreuses variantes. Existe-t-il un meilleur moyen de le faire sans utiliser autant d'arguments de mots-clés? Renommer chaque fonction est assez mauvais aussi parce que vous obtenez soit add_bullet1
, add_bullet2
ou add_bullet_with_really_long_name
.
Pour répondre à quelques réponses:
Non, je ne peux pas créer de hiérarchie de classe Bullet parce que c'est trop lent. Le code de gestion des puces est en C et mes fonctions sont des wrappers autour de l’API C.
Je connais les arguments de mots clés, mais vérifier toutes sortes de combinaisons de paramètres devient ennuyeux, mais les arguments par défaut aident à allouer comme acceleration=0
Ce que vous demandez est appelé plusieurs envois . Voir Julia Des exemples de langues illustrant différents types de dépêches.
Cependant, avant de regarder cela, nous allons d'abord expliquer pourquoi la surcharge n'est pas vraiment ce que vous voulez en python.
Tout d’abord, il faut comprendre le concept de surcharge et pourquoi il n’est pas applicable à Python.
Lorsque vous travaillez avec des langages capables de discriminer les types de données au moment de la compilation, vous pouvez choisir parmi les alternatives au moment de la compilation. Le fait de créer de telles fonctions alternatives pour la sélection au moment de la compilation est généralement appelé surcharge d’une fonction. ( Wikipedia )
Python est un langage typé dynamiquement , donc le concept de surcharge ne lui est tout simplement pas applicable. Cependant, tout n'est pas perdu, car nous pouvons créer de telles fonctions alternatives à l'exécution:
Dans les langages de programmation différant l'identification du type de données jusqu'au moment de l'exécution, la sélection parmi les fonctions alternatives doit avoir lieu au moment de l'exécution, en fonction des types d'arguments de fonction déterminés de manière dynamique. Les fonctions dont les implémentations alternatives sont sélectionnées de cette manière sont appelées plus généralement multiméthodes . ( Wikipedia )
Nous devrions donc pouvoir faire plusieurs méthodes dans python ou, comme on l'appelle alternativement, envoi multiple .
Les méthodes multiples sont également appelées plusieurs envois :
La répartition multiple ou méthodes multiples est la caractéristique de certains langages de programmation orientés objet dans lesquels une fonction ou une méthode peut être distribuée dynamiquement en fonction du type d'exécution (dynamique) de plusieurs de ses arguments. ( Wikipedia )
Python ne supporte pas cela immédiatement1. Mais, en l'occurrence, il existe un excellent package python appelé multipledispatch qui fait exactement cela.
Voici comment nous pourrions utiliser multipledispatch2 package pour implémenter vos méthodes:
>>> 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 prend actuellement en charge distribution unique
2. Veillez à ne pas utiliser multipledispatch dans un environnement multithreads, sinon vous obtiendrez un comportement étrange.
Python supporte la "surcharge de méthode" telle que vous la présentez. En fait, ce que vous venez de décrire est trivial à implémenter en Python, de tant de manières différentes, mais je choisirais:
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
Dans le code ci-dessus, default
est une valeur plausible par défaut pour ces arguments, ou None
. Vous pouvez ensuite appeler la méthode avec uniquement les arguments qui vous intéressent, et Python utilisera les valeurs par défaut.
Vous pouvez aussi faire quelque chose comme ça:
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
Une autre alternative consiste à relier directement la fonction souhaitée directement à la classe ou à l'instance:
def some_implementation(self, arg1, arg2, arg3):
# implementation
my_class.add_bullet = some_implementation_of_add_bullet
Une autre méthode consiste à utiliser un motif d'usine abstrait:
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
Vous pouvez utiliser la solution "rouler vous-même" pour la surcharge de fonctions. Celui-ci est copié de article de Guido van Rossum sur les méthodes multiples (car il y a peu de différence entre mm et une surcharge 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
L'usage serait
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):
# ...
Les limitations les plus restrictives actuellement sont:
Une option possible consiste à utiliser le module multipledispatch comme détaillé ici: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch
Au lieu de faire ceci:
def add(self, other):
if isinstance(other, Foo):
...
Elif isinstance(other, Bar):
...
else:
raise NotImplementedError()
Tu peux le faire:
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)
Avec l'utilisation résultante:
>>> add(1, 2)
3
>>> add(1, 'hello')
'1 + hello'
Dans Python3.4 a été ajouté PEP-0443. Fonctions génériques à distribution unique .
Voici une courte description de l'API de PEP.
Pour définir une fonction générique, décorez-la avec le décorateur @singledispatch. Notez que la répartition se produit sur le type du premier argument. Créez votre fonction en conséquence:
from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
if verbose:
print("Let me just say,", end=" ")
print(arg)
Pour ajouter des implémentations surchargées à la fonction, utilisez l'attribut register () de la fonction générique. C'est un décorateur, prenant un paramètre de type et décorant une fonction implémentant l'opération pour ce type:
@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)
Ce type de comportement est généralement résolu (en OOP langages) à l'aide de Polymorphism. Chaque type de balle serait responsable de savoir comment elle se déplace. Par exemple:
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..
}
Passez autant d'arguments existants à la fonction c_function, puis déterminez la fonction c à appeler en fonction des valeurs de la fonction c initiale. Ainsi, python ne devrait appeler que la seule fonction c. Cette fonction c examine les arguments et peut ensuite déléguer à d’autres fonctions c de manière appropriée.
Vous utilisez essentiellement chaque sous-classe comme un conteneur de données différent, mais en définissant tous les arguments potentiels de la classe de base, les sous-classes sont libres d'ignorer celles avec lesquelles elles ne font rien.
Lorsqu'un nouveau type de puce se présente, vous pouvez simplement définir une propriété supplémentaire sur la base, modifier la fonction python de sorte qu'elle transmette la propriété supplémentaire et la seule fonction c_function qui examine les arguments et les délégués. . Ça ne semble pas trop mal, je suppose.
Par en passant les arguments de mots clés .
def add_bullet(**kwargs):
#check for the arguments listed above and do the proper things
Je pense que votre exigence de base est d’avoir une syntaxe similaire à C/C++ dans python avec le moins de maux de tête possible. J'ai bien aimé la réponse d'Alexander Poluektov, mais cela ne fonctionne pas pour les cours.
Ce qui suit devrait fonctionner pour les cours. Cela fonctionne en distinguant par le nombre d'arguments non mot-clé (mais ne supporte pas la distinction par type):
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
Et il peut être utilisé simplement comme ceci:
test = TestOverloading()
test.overloaded_function("I'm a Sprite", 0, "Right")
print
test.overloaded_function("I'm another Sprite", "while x == True: print 'hi'")
Sortie:
C'est surcharge 3
Sprite: Je suis un Sprite
Début: 0
Direction: droiteC'est surcharge 2
Sprite: Je suis un autre Sprite
Scénario:
while x == True: affiche 'salut'
Je pense qu'une hiérarchie de classes Bullet
avec le polymorphisme associé est la voie à suivre. Vous pouvez effectivement surcharger le constructeur de la classe de base en utilisant une métaclasse afin que l'appel de la classe de base entraîne la création de l'objet de sous-classe approprié. Vous trouverez ci-dessous un exemple de code illustrant l’essence de ce que je veux dire.
Mise à jour
Le code a été modifié pour s'exécuter sous Python 2 et 3 afin de le maintenir pertinent. Cela a été fait d'une manière qui évite d'utiliser la syntaxe de métaclasse explicite de Python, qui varie entre les deux versions.
Pour atteindre cet objectif, une instance BulletMetaBase
de la classe BulletMeta
est créée en appelant explicitement la métaclasse lors de la création de la base Bullet
(au lieu d'utiliser l'attribut __metaclass__=
class ou via un argument de mot clé metaclass
dépendant de la version 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__.__+ ".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')
Sortie:
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__.__+ ".fire() method")
NotImplementedError: Bullet2.fire() method
Le décorateur @overload
a été ajouté avec des indications de type (PEP 484). Bien que cela ne change pas le comportement de python, cela facilite la compréhension de ce qui se passe et permet à mypy de détecter les erreurs.
Voir: indications de type et PEP 484
Utilisez plusieurs arguments de mot clé dans la définition ou créez une hiérarchie Bullet
dont les instances sont transmises à la fonction.
la surcharge des méthodes est délicate en python. Cependant, il pourrait être utilisé de passer les variables dict, list ou primitives.
J'ai essayé quelque chose pour mes cas d'utilisation, cela pourrait aider ici à comprendre les gens à surcharger les méthodes.
Prenons votre exemple:
une méthode de surcharge de classe avec appel des méthodes de classe différente.
def add_bullet(Sprite=None, start=None, headto=None, spead=None, acceleration=None):
passer les arguments de la classe distante:
add_bullet(Sprite = 'test', start=Yes,headto={'lat':10.6666,'long':10.6666},accelaration=10.6}
OR
add_bullet(Sprite = 'test', start=Yes, headto={'lat':10.6666,'long':10.6666},speed=['10','20,'30']}
Ainsi, la manipulation est en cours pour les variables de liste, de dictionnaire ou primitives provenant de la surcharge de méthodes.
essayez-le pour vos codes.
Utilisez des arguments de mots clés avec des valeurs par défaut. Par exemple.
def add_bullet(Sprite, start=default, direction=default, script=default, speed=default):
Dans le cas d'une balle droite par rapport à une balle courbe, j'ajouterais deux fonctions: add_bullet_straight
et add_bullet_curved
.
Juste un simple décorateur
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)
Vous pouvez l'utiliser comme ça
@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)
Modifiez-le pour l'adapter à votre cas d'utilisation.
Une clarification des concepts
self/this
. Cependant, la plupart des langues ne le font que pour l'argument this
. Le décorateur ci-dessus étend l'idée à plusieurs paramètres.Pour éclaircir, adopter un langage statique et définir les fonctions
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)
Avec la répartition statique (surcharge), vous verrez le "numéro appelé" deux fois, parce que x
a été déclaré comme Number
et que tout le monde se soucie de la surcharge. Avec la répartition dynamique, vous verrez "entier appelé, float appelé", car ce sont les types réels de x
au moment où la fonction est appelée.