Surcharge de la fonction Python

213

Je sais que Python ne prend pas en charge la surcharge de méthode, mais j'ai rencontré un problème que je n'arrive pas à résoudre d'une manière sympa Pythonic.

Je fais un jeu où un personnage doit tirer une variété de 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 vers 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 une meilleure façon de le faire sans utiliser autant d'arguments de mots clés, cela devient un peu laid rapidement. Changement de nom chaque fonction est assez mal aussi parce que vous obtenez soit add_bullet1, add_bullet2ou add_bullet_with_really_long_name.

Pour répondre à certaines réponses:

  1. Non, je ne peux pas créer une hiérarchie de classes Bullet car c'est trop lent. Le code réel pour la gestion des puces est en C et mes fonctions sont des wrappers autour de l'API C.

  2. Je connais les arguments des mots clés, mais vérifier toutes sortes de combinaisons de paramètres devient ennuyeux, mais les arguments par défaut aident à attribuer comme acceleration=0

Balles
la source
5
Fonctionne pour un seul paramètre, mais ici (pour les personnes venant ici d'un moteur de recherche): docs.python.org/3/library/…
leewz
1
cela semble être un bon endroit pour les valeurs par défaut. vous pouvez définir certains sur Aucun et simplement les vérifier. l'impact extra-booléen semble négligeable
Andrew Scott Evans
Doit utiliser default value + if + elsepour faire la même chose que C ++. C'est l'une des très rares choses que C ++ a une meilleure lisibilité que Python ...
Deqing
Je ne comprends pas pourquoi kwargs n'est pas une réponse valide. Vous dites que vous ne voulez pas utiliser beaucoup d'arguments de mots-clés car cela devient très rapide ... eh bien c'est juste la nature du problème. Si vous avez beaucoup d'arguments et que c'est désordonné car vous avez beaucoup d'arguments que vous attendiez? Voulez-vous utiliser de nombreux arguments sans les spécifier n'importe où ??? Python n'est pas un lecteur d'esprit.
Calcul
Nous ne savons pas quels types d'objets script, curvesont, ont-ils un ancêtre commun, quelles méthodes ils prennent en charge. Avec la frappe de canard, c'est à vous de concevoir la classe pour déterminer les méthodes dont ils ont besoin pour prendre en charge. ScriptPrend probablement en charge une sorte de rappel basé sur le pas de temps (mais quel objet doit-il retourner? La position à ce pas de temps? La trajectoire à ce pas de temps?). Vraisemblablement start, direction, speed, les start, headto, spead, accelerationdeux décrivent les types de trajectoires, mais encore une fois, c'est à vous de concevoir la classe de réception pour savoir comment les déballer et les traiter.
smci

Réponses:

144

Ce que vous demandez s'appelle l' envoi multiple . Voir des exemples de langage Julia qui illustrent différents types de dépêches.

Cependant, avant de regarder cela, nous aborderons d'abord pourquoi la surcharge n'est pas vraiment ce que vous voulez en python.

Pourquoi ne pas surcharger?

Tout d'abord, il faut comprendre le concept de surcharge et pourquoi il ne s'applique pas au python.

Lorsque vous travaillez avec des langages qui peuvent discriminer les types de données au moment de la compilation, la sélection parmi les alternatives peut se produire au moment de la compilation. L'acte 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 s'applique tout simplement pas à lui. Cependant, tout n'est pas perdu, car nous pouvons créer de telles fonctions alternatives au moment de l'exécution:

Dans les langages de programmation qui reportent l'identification du type de données jusqu'à 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 dynamiquement. Les fonctions dont les implémentations alternatives sont sélectionnées de cette manière sont appelées le plus généralement multiméthodes . ( Wikipedia )

Nous devrions donc être capables de faire des multiméthodes en python - ou, comme on l'appelle alternativement: la répartition multiple .

Envoi multiple

Les multiméthodes sont également appelées expédition multiple :

La répartition multiple ou plusieurs méthodes 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 prend pas cela en charge dès la sortie de la boîte 1 , mais, en l'occurrence, il existe un excellent package python appelé multipledispatch qui fait exactement cela.

Solution

Voici comment nous pourrions utiliser le package multipledispatch 2 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 la répartition unique. 2. Faites attention de ne pas utiliser la diffusion multiple dans un environnement multi-thread ou vous obtiendrez un comportement étrange.

Andriy Drozdyuk
la source
6
Quel est le problème avec 'multipledispatch' dans un environnement multi-thread? Puisque le code côté serveur est généralement dans un environnement multi-thread! J'essaye juste de le creuser!
danzeer
7
@danzeer Ce n'était pas thread-safe. J'ai vu l'argument être modifié par deux threads différents (c'est-à-dire que la valeur de speedpourrait changer au milieu de la fonction lorsqu'un autre thread définit sa propre valeur despeed ) !!! Il m'a fallu beaucoup de temps pour réaliser que c'était la bibliothèque qui était le coupable.
Andriy Drozdyuk
108

Python prend en charge 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 façons différentes, mais j'irais avec:

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, defaultest une valeur par défaut plausible 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 également faire quelque chose comme ceci:

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 à accrocher 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

Encore une autre façon consiste à utiliser un modèle 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 
Escualo
la source
107
Tout cela ressemble à des exemples d'arguments variables, plutôt qu'à une surcharge. Depuis la surcharge vous permet d'avoir la même fonction, pour différents types d'arguments. par exemple: sum (real_num1, real_num2) et sum (imaginary_num1, imaginary_num2) auront tous deux la même syntaxe d'appel, mais attendent en réalité 2 types différents en entrée, et l'implémentation doit également changer en interne
Efren
17
En utilisant la réponse avec laquelle vous iriez, comment présenteriez-vous à l'appelant quels arguments ont du sens ensemble? Le simple fait de mettre un tas d'arguments chacun avec une valeur par défaut peut fournir la même fonctionnalité mais en termes d'API, il est beaucoup moins élégant
Greg Ennis
6
Rien de ce qui précède n'est en surcharge, l'implémentation devra vérifier toutes les combinaisons d'entrées de paramètres (ou ignorer les paramètres) comme: if sprite and script and not start and not direction and not speed...juste pour savoir qu'il s'agit d'une action spécifique. car un appelant peut appeler la fonction en fournissant tous les paramètres disponibles. Pendant la surcharge, définissez pour vous les ensembles exacts de paramètres pertinents.
Roee Gavirel
5
C'est très bouleversant quand les gens disent que python prend en charge la surcharge de méthode. Ce ne est pas. Le fait que vous ayez mis "surcharge de méthode" entre guillemets indique que vous en êtes conscient. Vous pouvez obtenir des fonctionnalités similaires avec plusieurs techniques, comme celle mentionnée ici. Mais la surcharge de méthode a une définition très spécifique.
Howard Swope
Je pense que le but recherché est que si la surcharge de méthode n'est pas une caractéristique de python, les mécanismes ci-dessus peuvent être utilisés pour obtenir l'effet équivalent.
rawr a sonné le
93

Vous pouvez utiliser la solution "roll-your-own" pour la surcharge de fonctions. Celui-ci est copié de l'article de Guido van Rossum sur les multiméthodes (car il y a peu de différence entre mm et 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):
    # ...

Limitations les plus restrictives à l'heure actuelle sont:

  • les méthodes ne sont pas prises en charge, seules les fonctions qui ne sont pas des membres de classe;
  • l'héritage n'est pas géré;
  • les kwargs ne sont pas pris en charge;
  • l'enregistrement de nouvelles fonctions doit être effectué au moment de l'importation, la chose n'est pas adaptée aux threads
Alexander Poluektov
la source
6
+1 pour les décorateurs pour l'extension de la langue dans ce cas d'utilisation.
Eloims
1
+1 parce que c'est une excellente idée (et probablement avec quoi l'OP devrait aller) --- Je n'avais jamais vu une implémentation multiméthode en Python.
Escualo
39

Une option possible consiste à utiliser le module de répartition multiple 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'
Dave C
la source
4
Pourquoi cela n'obtient-il pas plus de votes? Je devine en raison du manque d'exemples ... J'ai créé une réponse avec un exemple de la façon de mettre en œuvre une solution au problème d'OP avec le package multipledispatch .
Andriy Drozdyuk
19

En Python 3.4 a été ajouté PEP-0443. Fonctions génériques à envoi 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. Il s'agit d'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)
SKhalymon
la source
11

Ce type de comportement est généralement résolu (dans les langages POO) en utilisant le polymorphisme. 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 à la fonction c_ qui existent, puis déterminez la fonction c à appeler en fonction des valeurs de la fonction c initiale. Ainsi, python ne devrait jamais appeler que la fonction one c. Cette fonction c examine les arguments, puis peut 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 sur la classe de base, les sous-classes sont libres d'ignorer celles avec lesquelles elles ne font rien.

Lorsqu'un nouveau type de puce arrive, vous pouvez simplement définir une propriété de plus sur la base, modifier la fonction python afin qu'elle transmette la propriété supplémentaire et la fonction c_function qui examine les arguments et délègue de manière appropriée. Cela ne semble pas trop mal, je suppose.

Josh Smeaton
la source
1
C'était mon approche initiale, mais pour des raisons de performances, j'ai dû réécrire ce code en C.
Bullets
@Bullets, je dirais qu'il peut y avoir un certain nombre d'options différentes disponibles pour améliorer les performances plutôt que d'écrire un tas de fonctions c qui ne feront probablement pas beaucoup. Par exemple: la création d'une instance peut être coûteuse, alors conservez un pool d'objets. Bien que je dis cela sans savoir ce que vous avez trouvé trop lent. Par intérêt, quelle a été la lenteur de cette approche? À moins que beaucoup de temps ne soit passé du côté C de la frontière, je ne peux pas penser que Python (lui-même) soit le vrai problème.
Josh Smeaton
Il y a peut-être d'autres façons d'améliorer les performances, mais je suis bien meilleur avec C qu'avec Python. Le problème était de calculer les mouvements des balles et de détecter quand elles sortent des limites de l'écran. J'avais une méthode pour calculer la position de la balle pos+v*tet ensuite la comparer aux limites de l'écran if x > 800et ainsi de suite. L'appel de ces fonctions plusieurs centaines de fois par image s'est avéré être trop lent. C'était quelque chose comme 40 fps à 100% cpu avec du python pur à 60 fps avec 5% -10% quand c'est fait en C.
Bullets
@Bullets, assez juste alors. J'utiliserais toujours l'approche que j'ai choisie pour encapsuler les données. Passez une instance de puce à add_bulletet extrayez tous les champs dont vous avez besoin. Je vais modifier ma réponse.
Josh Smeaton
@Bullets: Vous pouvez combiner vos fonctions C et l'approche POO suggérée par Josh en utilisant Cython . Il permet une liaison anticipée donc il ne devrait pas y avoir de pénalité de vitesse.
jfs
4

Soit utiliser plusieurs arguments de mot-clé dans la définition, soit créer une Bullethiérarchie dont les instances sont passées à la fonction.

Ignacio Vazquez-Abrams
la source
J'allais suggérer la deuxième approche: créer des classes BulletParams ... pour spécifier les détails de la balle.
John Zwinck
Pouvez-vous élaborer sur ce sujet? J'ai essayé de créer une hiérarchie de classes avec différentes puces, mais cela ne fonctionne pas, car Python est trop lent. Il ne peut pas calculer les mouvements du nombre de balles requis assez rapidement, j'ai donc dû écrire cette partie en C. Toutes les variantes add_bullet appellent simplement la fonction C correspondante.
Bullets
4

Je pense que votre exigence de base est d'avoir une syntaxe similaire à C / C ++ en python avec le moins de maux de tête possible. Bien que j'aie aimé la réponse d'Alexander Poluektov, cela ne fonctionne pas pour les cours.

Les éléments suivants devraient fonctionner pour les classes. Il fonctionne en distinguant par le nombre d'arguments non-mot-clé (mais ne prend pas en charge 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'")

Production:

C'est la surcharge 3
Sprite: Je suis un Sprite
Début: 0
Direction: Droite

C'est la surcharge 2
Sprite: je suis un autre Sprite
Script:
tandis que x == True: affichez 'salut'

Tim Ludwinski
la source
4

Le @overloaddécorateur a été ajouté avec des indices 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: Conseils de type et PEP 484

Richard Whitehead
la source
Pouvez-vous ajouter quelques exemples?
gerrit
3

Je pense qu'une Bullethiérarchie de classes 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é. Voici un exemple de code pour illustrer l'essence de ce que je veux dire.

Actualisé

Le code a été modifié pour fonctionner sous Python 2 et 3 pour le garder pertinent. Cela a été fait d'une manière qui évite d'utiliser la syntaxe explicite de métaclasse de Python, qui varie entre les deux versions.

Pour atteindre cet objectif, une BulletMetaBaseinstance de la BulletMetaclasse est créée en appelant explicitement la métaclasse lors de la création de la Bulletclasse de base (plutôt qu'en utilisant l' __metaclass__=attribut class ou via un metaclassargument de mot clé selon la version de 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')

Production:

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
martineau
la source
Hmm, ce n'est encore qu'un moyen sophistiqué de nommer les fonctions add_bullet1, add_bullet2 et ainsi de suite.
Bullets
1
@Bullets: C'est peut-être le cas, ou c'est peut-être juste une façon légèrement élaborée de créer une fonction d'usine. Une bonne chose à ce sujet est qu'il prend en charge une hiérarchie de Bulletsous - classes sans avoir à modifier la classe de base ou la fonction d'usine à chaque fois que vous ajoutez un autre sous-type. (Bien sûr, si vous utilisez C plutôt que C ++, je suppose que vous n'avez pas de classes.) Vous pouvez également créer une métaclasse plus intelligente qui détermine par elle-même la sous-classe à créer en fonction du type et / ou du nombre des arguments passés (comme le fait C ++ pour supporter la surcharge).
martineau
1
Cette idée d'héritage serait également ma première option.
Daniel Möller
3

Python 3.8 a ajouté functools.singledispatchmethod

Transformez une méthode en une fonction générique à envoi unique.

Pour définir une méthode générique, décorez-la avec le décorateur @singledispatchmethod. Notez que la répartition se produit sur le type du premier argument non-self ou non-cls, créez votre fonction en conséquence:

from functools import singledispatchmethod


class Negator:
    @singledispatchmethod
    def neg(self, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(self, arg: int):
        return -arg

    @neg.register
    def _(self, arg: bool):
        return not arg


negator = Negator()
for v in [42, True, "Overloading"]:
    neg = negator.neg(v)
    print(f"{v=}, {neg=}")

Production

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a

@singledispatchmethod prend en charge l'imbrication avec d'autres décorateurs tels que @classmethod. Notez que pour permettre dispatcher.register, singledispatchmethod doit être le décorateur le plus extérieur. Voici la classe Negator avec les méthodes neg liées à la classe:

from functools import singledispatchmethod


class Negator:
    @singledispatchmethod
    @staticmethod
    def neg(arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(arg: int) -> int:
        return -arg

    @neg.register
    def _(arg: bool) -> bool:
        return not arg


for v in [42, True, "Overloading"]:
    neg = Negator.neg(v)
    print(f"{v=}, {neg=}")

Production:

v=42, neg=-42
v=True, neg=False
NotImplementedError: Cannot negate a

Le même motif peut être utilisé pour d'autres décorateurs similaires: méthode statique, méthode abstraite et autres.

Vlad Bezden
la source
2

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_straightet add_bullet_curved.

Rafe Kettler
la source
2

la surcharge des méthodes est délicate en python. Cependant, il pourrait être utile 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 pour surcharger les méthodes.

Prenons votre exemple:

une méthode de surcharge de classe avec appel aux méthodes de classe différente.

def add_bullet(sprite=None, start=None, headto=None, spead=None, acceleration=None):

passez les arguments de la classe distante:

add_bullet(sprite = 'test', start=Yes,headto={'lat':10.6666,'long':10.6666},accelaration=10.6}

OU

add_bullet(sprite = 'test', start=Yes, headto={'lat':10.6666,'long':10.6666},speed=['10','20,'30']}

Ainsi, la gestion des variables de liste, de dictionnaire ou primitives est en cours de surcharge de méthode.

essayez-le pour vos codes.

shashankS
la source
2

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 ceci

@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

  • répartition des fonctions : il existe plusieurs fonctions portant le même nom. Lequel devrait être appelé? deux stratégies
  • répartition statique / au moment de la compilation ( aka. "surcharge" ). Décidez quelle fonction appeler en fonction du type de compilation des arguments. Dans tous les langages dynamiques, il n'y a pas de type au moment de la compilation, donc la surcharge est impossible par définition
  • répartition dynamique / au moment de l' exécution : décidez de la fonction à appeler en fonction du type d' exécution des arguments. C'est ce que font tous les langages POO: plusieurs classes ont les mêmes méthodes, et le langage décide lequel appeler en fonction du type d' self/thisargument. Cependant, la plupart des langues ne le font que pour l' thisargument. Le décorateur ci-dessus étend l'idée à plusieurs paramètres.

Pour clarifier, assumer un langage statique et définir les fonctions

void f(Integer x):
    print('integer 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 deux fois "numéro appelé", car xa été déclaré comme Number, et c'est tout ce qui compte pour la surcharge. Avec la répartition dynamique, vous verrez "entier appelé, float appelé", car ce sont les types réels xau moment où la fonction est appelée.

note bleue
la source
Cet exemple n'illustre pas de manière cruciale quelle méthode a été appelée xpour la répartition dynamique, ni dans quel ordre les deux méthodes ont été appelées pour la répartition statique. Je vous recommande de modifier les relevés d'impression en print('number called for Integer')etc.
smci