web-dev-qa-db-fra.com

Python surcharge de fonction

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:

  1. 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.

  2. 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

170
Bullets

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.

Pourquoi ne pas surcharger?

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 .

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.

Solution

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.

109
Andriy Drozdyuk

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 
104
Escualo

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:

  • les méthodes ne sont pas prises en charge, seules les fonctions qui ne sont pas membres de la classe;
  • l'héritage n'est pas traité;
  • les kwargs ne sont pas supportés;
  • l'enregistrement de nouvelles fonctions doit être effectué au moment de l'importation, ce qui n'est pas thread-safe
84

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'
34
Dave C

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)
15
Infernion

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.

11
Josh Smeaton

Par en passant les arguments de mots clés .

def add_bullet(**kwargs):
    #check for the arguments listed above and do the proper things
6
Nick Radford

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: droite

C'est surcharge 2
Sprite: Je suis un autre Sprite
Scénario:
while x == True: affiche 'salut'

4
Tim Ludwinski

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
3
martineau

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

3
Richard Whitehead

Utilisez plusieurs arguments de mot clé dans la définition ou créez une hiérarchie Bullet dont les instances sont transmises à la fonction.

3

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.

2
shashankS

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.

2
Rafe Kettler

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

  • dispatch de fonction : il existe plusieurs fonctions portant le même nom. Lequel devrait être appelé? deux stratégies
  • statique/envoi au moment de la compilation (aka "surcharge"). décidez quelle fonction appeler en fonction du type au moment de la compilation des arguments. Dans tous les langages dynamiques, il n'y a pas de type à la compilation, donc la surcharge est impossible par définition
  • dispatch dynamique/run-time : choisissez la fonction à appeler en fonction du runtime type des arguments. C’est ce que font tous les langages OOP: plusieurs classes utilisent les mêmes méthodes et le langage décide lequel appeler en fonction du type d’argument 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.

0
blue_note