Überladung der Python-Funktion

212

Ich weiß, dass Python das Überladen von Methoden nicht unterstützt, aber ich bin auf ein Problem gestoßen, das ich scheinbar nicht auf nette pythonische Weise lösen kann.

Ich mache ein Spiel, in dem ein Charakter eine Vielzahl von Kugeln abschießen muss, aber wie schreibe ich verschiedene Funktionen zum Erstellen dieser Kugeln? Angenommen, ich habe eine Funktion, die eine Kugel erzeugt, die sich mit einer bestimmten Geschwindigkeit von Punkt A nach B bewegt. Ich würde eine Funktion wie diese schreiben:

    def add_bullet(sprite, start, headto, speed):
        ... Code ...

Aber ich möchte andere Funktionen zum Erstellen von Aufzählungszeichen schreiben wie:

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

Und so weiter mit vielen Variationen. Gibt es einen besseren Weg, dies zu tun, ohne so viele Schlüsselwortargumente zu verwenden, dass es schnell hässlich wird? Jede Funktion Umbenennen ist ziemlich schlecht auch , weil Sie entweder add_bullet1, add_bullet2oder add_bullet_with_really_long_name.

Um einige Antworten zu finden:

  1. Nein, ich kann keine Bullet-Klassenhierarchie erstellen, da dies zu langsam ist. Der eigentliche Code zum Verwalten von Aufzählungszeichen befindet sich in C, und meine Funktionen sind Wrapper für die C-API.

  2. Ich kenne die Schlüsselwortargumente, aber das Überprüfen auf alle möglichen Kombinationen von Parametern wird ärgerlich, aber Standardargumente helfen bei der Auswahl acceleration=0

Kugeln
quelle
5
Funktioniert nur für einen Parameter, aber hier (für Leute, die von einer Suchmaschine hierher kommen): docs.python.org/3/library/…
leewz
1
Dies scheint ein guter Ort für Standardwerte zu sein. Sie können einige auf Keine setzen und einfach nach ihnen suchen. Die zusätzliche boolesche Auswirkung scheint vernachlässigbar
Andrew Scott Evans
Müssen verwenden default value + if + else, um das gleiche wie C ++ zu tun. Dies ist eines der wenigen Dinge, die C ++ besser lesbar macht als Python ...
Deqing
Ich bin verwirrt darüber, warum kwargs keine gültige Antwort ist. Sie sagen, dass Sie nicht viele Keyword-Argumente verwenden möchten, weil es schnell hässlich wird ... nun, das ist nur die Natur des Problems. Wenn Sie viele Argumente haben und es chaotisch ist, weil Sie viele Argumente haben, als Sie erwartet haben? Möchten Sie viele Argumente verwenden, ohne sie irgendwo anzugeben? Python ist kein Gedankenleser.
Kalkül
Wir wissen nicht, was für Objekte es script, curvesind, haben sie einen gemeinsamen Vorfahren, welche Methoden sie unterstützen. Beim Entenschreiben liegt es an Ihnen, für das Klassendesign herauszufinden, welche Methoden sie unterstützen müssen. ScriptUnterstützt vermutlich eine Art zeitschrittbasierten Rückruf (aber welches Objekt sollte es zurückgeben? Die Position zu diesem Zeitschritt? Die Flugbahn zu diesem Zeitschritt?). Vermutlich start, direction, speedund start, headto, spead, accelerationbeide beschreiben Arten von Trajektorien, aber es liegt wieder an Ihnen, die empfangende Klasse so zu gestalten, dass Sie wissen, wie Sie sie entpacken und verarbeiten können.
smci

Antworten:

143

Was Sie verlangen, wird als Mehrfachversand bezeichnet . Siehe Julia- Sprachbeispiele, die verschiedene Arten von Versendungen demonstrieren.

Bevor wir uns das ansehen, werden wir uns zunächst damit befassen, warum Überladung in Python nicht wirklich das ist, was Sie wollen.

Warum nicht überladen?

Zunächst muss man das Konzept der Überladung verstehen und wissen, warum es nicht auf Python anwendbar ist.

Wenn Sie mit Sprachen arbeiten, die Datentypen zur Kompilierungszeit unterscheiden können, kann zur Kompilierungszeit eine Auswahl unter den Alternativen erfolgen. Das Erstellen solcher alternativen Funktionen für die Auswahl zur Kompilierungszeit wird üblicherweise als Überladen einer Funktion bezeichnet. ( Wikipedia )

Python ist eine dynamisch typisierte Sprache, daher gilt das Konzept der Überladung einfach nicht für sie. Es ist jedoch nicht alles verloren, da wir zur Laufzeit solche alternativen Funktionen erstellen können :

In Programmiersprachen, die die Datentypidentifikation bis zur Laufzeit verschieben, muss die Auswahl unter alternativen Funktionen zur Laufzeit erfolgen, basierend auf den dynamisch bestimmten Arten von Funktionsargumenten. Funktionen, deren alternative Implementierungen auf diese Weise ausgewählt werden, werden am allgemeinsten als Multimethoden bezeichnet . ( Wikipedia )

Wir sollten also in der Lage sein, Multimethoden in Python auszuführen - oder, wie es alternativ heißt: Mehrfachversand .

Mehrfachversand

Die Multimethoden werden auch als Mehrfachversand bezeichnet :

Mehrfachversand oder Mehrfachmethoden sind das Merkmal einiger objektorientierter Programmiersprachen, in denen eine Funktion oder Methode basierend auf dem Laufzeittyp (dynamisch) von mehr als einem ihrer Argumente dynamisch versendet werden kann. ( Wikipedia )

Python unterstützt diese aus der Box 1 , aber, wie es geschieht, gibt es ein ausgezeichnetes Python - Paket namens multipledispatch , die das genau funktioniert.

Lösung

So können wir das Multipledispatch 2- Paket verwenden, um Ihre Methoden zu implementieren:

>>> 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 unterstützt derzeit den Einzelversand.
2. Achten Sie darauf, dass Sie in einer Umgebung mit mehreren Threads keinen Mehrfachversand verwenden. Andernfalls tritt ein seltsames Verhalten auf.

Andriy Drozdyuk
quelle
6
Was ist das Problem mit "Multipledispatch" in einer Multithread-Umgebung? Da sich der Code auf der Serverseite normalerweise in einer Multithread-Umgebung befindet! Ich versuche nur, es auszuheben!
Danzeer
7
@danzeer Es war nicht threadsicher. Ich habe gesehen, dass das Argument durch zwei verschiedene Threads geändert wurde (dh der Wert von speedkann sich in der Mitte der Funktion ändern, wenn ein anderer Thread seinen eigenen Wert von festlegt speed) !!! Es dauerte lange, bis mir klar wurde, dass die Bibliothek der Schuldige war.
Andriy Drozdyuk
108

Python unterstützt das "Überladen von Methoden", wenn Sie es präsentieren. Tatsächlich ist das, was Sie gerade beschreiben, in Python auf so viele verschiedene Arten trivial zu implementieren, aber ich würde folgendermaßen vorgehen:

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

Im obigen Code defaultist ein plausibler Standardwert für diese Argumente oder None. Sie können die Methode dann nur mit den Argumenten aufrufen, an denen Sie interessiert sind, und Python verwendet die Standardwerte.

Sie könnten auch so etwas tun:

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

Eine andere Alternative besteht darin, die gewünschte Funktion direkt mit der Klasse oder Instanz zu verknüpfen:

def some_implementation(self, arg1, arg2, arg3):
  # implementation
my_class.add_bullet = some_implementation_of_add_bullet

Ein weiterer Weg ist die Verwendung eines abstrakten Fabrikmusters:

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
quelle
107
All dies ist eher ein Beispiel für variable Argumente als für eine Überladung. Da Überladung ermöglicht es Ihnen, die gleiche Funktion für verschiedene Typen als Argumente zu haben. Beispiel: sum (real_num1, real_num2) und sum (imaginary_num1, imaginary_num2) haben beide dieselbe Aufrufsyntax, erwarten jedoch tatsächlich 2 verschiedene Typen als Eingabe, und die Implementierung muss sich auch intern ändern
Efren
17
Wie würden Sie dem Anrufer anhand der Antwort, mit der Sie gehen würden, präsentieren, welche Argumente zusammen sinnvoll sind? Nur eine Reihe von Argumenten mit jeweils einem Standardwert zu setzen, bietet möglicherweise die gleiche Funktionalität, ist jedoch in Bezug auf eine API viel weniger elegant
Greg Ennis,
6
Wenn keines der oben genannten Elemente überladen ist, muss die Implementierung alle Kombinationen von Parametereingaben überprüfen (oder Parameter ignorieren), z. B.: if sprite and script and not start and not direction and not speed...Nur um zu wissen, dass es sich um eine bestimmte Aktion handelt. weil ein Aufrufer die Funktion aufrufen kann, die alle verfügbaren Parameter bereitstellt. Definieren Sie beim Überladen für Sie die genauen Sätze relevanter Parameter.
Roee Gavirel
5
Es ist sehr ärgerlich, wenn Leute sagen, dass Python das Überladen von Methoden unterstützt. Es tut nicht. Die Tatsache, dass Sie "Methodenüberladung" in Anführungszeichen setzen, zeigt an, dass Sie sich dieser Tatsache bewusst sind. Sie können ähnliche Funktionen mit verschiedenen Techniken erhalten, wie der hier erwähnten. Das Überladen von Methoden hat jedoch eine sehr spezifische Definition.
Howard Swope
Ich denke, der beabsichtigte Punkt ist, dass während das Überladen von Methoden kein Merkmal von Python ist, die oben genannten Mechanismen verwendet werden können, um den äquivalenten Effekt zu erzielen.
Rawr klingelte am
93

Sie können die "Roll-Your-Own" -Lösung zur Funktionsüberladung verwenden. Dieser ist aus Guido van Rossums Artikel über Multimethoden kopiert (da es in Python kaum einen Unterschied zwischen mm und Überladung gibt):

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

Die Verwendung wäre

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):
    # ...

Die derzeit restriktivsten Einschränkungen sind:

  • Methoden werden nicht unterstützt, nur Funktionen, die keine Klassenmitglieder sind.
  • Vererbung wird nicht behandelt;
  • kwargs werden nicht unterstützt;
  • Das Registrieren neuer Funktionen sollte zum Zeitpunkt des Imports erfolgen, da dies nicht threadsicher ist
Alexander Poluektov
quelle
6
+1 für Dekorateure zur Erweiterung der Sprache in diesem Anwendungsfall.
Eloims
1
+1, weil dies eine großartige Idee ist (und wahrscheinlich, wie das OP aussehen sollte) --- Ich hatte noch nie eine Multimethoden-Implementierung in Python gesehen.
Escualo
39

Eine mögliche Option ist die Verwendung des hier beschriebenen Multipledispatch-Moduls: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch

Anstatt dies zu tun:

def add(self, other):
    if isinstance(other, Foo):
        ...
    elif isinstance(other, Bar):
        ...
    else:
        raise NotImplementedError()

Du kannst das:

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)

Mit der daraus resultierenden Verwendung:

>>> add(1, 2)
3

>>> add(1, 'hello')
'1 + hello'
Dave C.
quelle
4
Warum bekommt das nicht mehr Stimmen? Ich vermute aufgrund fehlender Beispiele ... Ich habe eine Antwort mit einem Beispiel erstellt, wie eine Lösung für das OP-Problem mit dem Multipledispatch- Paket implementiert werden kann.
Andriy Drozdyuk
19

In Python 3.4 wurde PEP-0443 hinzugefügt . Generische Funktionen für den Einzelversand .

Hier ist eine kurze API-Beschreibung von PEP.

Um eine generische Funktion zu definieren, dekorieren Sie sie mit dem Dekorator @singledispatch. Beachten Sie, dass der Versand für den Typ des ersten Arguments erfolgt. Erstellen Sie Ihre Funktion entsprechend:

from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg)

Verwenden Sie das Attribut register () der generischen Funktion, um der Funktion überladene Implementierungen hinzuzufügen. Dies ist ein Dekorator, der einen Typparameter verwendet und eine Funktion dekoriert, die die Operation für diesen Typ implementiert:

@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
quelle
11

Diese Art von Verhalten wird normalerweise (in OOP-Sprachen) mithilfe von Polymorphismus gelöst. Jede Art von Kugel wäre dafür verantwortlich zu wissen, wie sie sich bewegt. Zum Beispiel:

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

Übergeben Sie so viele Argumente wie möglich an die vorhandene c_function und bestimmen Sie anhand der Werte in der anfänglichen c-Funktion, welche c-Funktion aufgerufen werden soll. Python sollte also immer nur die one c-Funktion aufrufen. Diese eine c-Funktion betrachtet die Argumente und kann dann entsprechend an andere c-Funktionen delegieren.

Sie verwenden im Wesentlichen nur jede Unterklasse als einen anderen Datencontainer. Wenn Sie jedoch alle potenziellen Argumente für die Basisklasse definieren, können die Unterklassen diejenigen ignorieren, mit denen sie nichts tun.

Wenn ein neuer Aufzählungszeichentyp hinzukommt, können Sie einfach eine weitere Eigenschaft in der Basis definieren, die Python-Funktion so ändern, dass sie die zusätzliche Eigenschaft übergibt, und die eine c_-Funktion, die die Argumente und Delegaten entsprechend untersucht. Klingt nicht schlecht, denke ich.

Josh Smeaton
quelle
1
Das war mein erster Ansatz, aber aus Leistungsgründen musste ich diesen Code in C.
Bullets
@Bullets, ich würde vorschlagen, dass es eine Reihe verschiedener Optionen gibt, um die Leistung zu verbessern, anstatt eine ganze Reihe von c-Funktionen zu schreiben, die wahrscheinlich nicht viel bewirken werden. Beispiel: Das Erstellen einer Instanz kann teuer sein. Verwalten Sie daher einen Objektpool. Obwohl ich das sage, ohne zu wissen, was Sie als zu langsam empfunden haben. Was genau war aus Interesse an diesem Ansatz langsam? Ich kann mir nicht vorstellen, dass Python (selbst) das eigentliche Problem ist, es sei denn, es wird viel Zeit auf der C-Seite der Grenze verbracht.
Josh Smeaton
Vielleicht gibt es andere Möglichkeiten, die Leistung zu verbessern, aber ich bin mit C viel besser als mit Python. Das Problem bestand darin, die Bewegungen der Kugeln zu berechnen und festzustellen, wann sie außerhalb der Bildschirmgrenzen liegen. Ich hatte Methoden, um die Position des Geschosses zu berechnen pos+v*tund dann mit Bildschirmgrenzen zu vergleichen if x > 800und so weiter. Das mehrmalige Aufrufen dieser Funktionen pro Frame erwies sich als unannehmbar langsam. Es war ungefähr 40 fps bei 100% CPU mit reinem Python bis 60 fps mit 5% -10%, wenn es in C gemacht wurde.
Bullets
@ Bullets, fair genug dann. Ich würde immer noch den Ansatz verwenden, mit dem ich Daten gekapselt habe. Übergeben Sie eine Instanz von Bullet an add_bulletund extrahieren Sie alle Felder, die Sie benötigen. Ich werde meine Antwort bearbeiten.
Josh Smeaton
@Bullets: Sie können Ihre C-Funktionen und den von Josh vorgeschlagenen OOP-Ansatz mit Cython kombinieren . Es ermöglicht eine frühzeitige Bindung, so dass es keine Geschwindigkeitsstrafe geben sollte.
JFS
4

Verwenden Sie entweder mehrere Schlüsselwortargumente in der Definition oder erstellen Sie eine BulletHierarchie, deren Instanzen an die Funktion übergeben werden.

Ignacio Vazquez-Abrams
quelle
Ich wollte den zweiten Ansatz vorschlagen: Erstellen Sie einige BulletParams ... -Klassen, um die Aufzählungsdetails anzugeben.
John Zwinck
Können Sie das näher erläutern? Ich habe versucht, eine Klassenhierarchie mit verschiedenen Aufzählungszeichen zu erstellen, aber dies funktioniert nicht, da Python zu langsam ist. Die Bewegungen der erforderlichen Anzahl von Aufzählungszeichen können nicht schnell genug berechnet werden, daher musste ich diesen Teil in C schreiben. Alle add_bullet-Varianten rufen nur die entsprechende C-Funktion auf.
Kugeln
4

Ich denke, Ihre Grundvoraussetzung ist eine C / C ++ - ähnliche Syntax in Python mit möglichst wenig Kopfschmerzen. Obwohl mir die Antwort von Alexander Poluektov gefallen hat, funktioniert sie im Unterricht nicht.

Folgendes sollte für Klassen funktionieren. Es funktioniert durch Unterscheiden nach der Anzahl der Nicht-Schlüsselwortargumente (unterstützt jedoch nicht die Unterscheidung nach Typ):

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

Und es kann einfach so verwendet werden:

test = TestOverloading()

test.overloaded_function("I'm a Sprite", 0, "Right")
print
test.overloaded_function("I'm another Sprite", "while x == True: print 'hi'")

Ausgabe:

Dies ist Überlastung 3
Sprite: Ich bin ein Sprite
Start: 0
Richtung: Richtig

Dies ist Überladung 2
Sprite: Ich bin ein anderes Sprite-
Skript:
while x == True: print 'hi'

Tim Ludwinski
quelle
4

Der @overloadDekorateur wurde mit Typhinweisen (PEP 484) hinzugefügt. Dies ändert zwar nichts am Verhalten von Python, erleichtert jedoch das Verständnis der Vorgänge und das Erkennen von Fehlern durch mypy.
Siehe: Tipphinweise und PEP 484

Richard Whitehead
quelle
Können Sie einige Beispiele hinzufügen?
Gerrit
3

Ich denke, eine BulletKlassenhierarchie mit dem damit verbundenen Polymorphismus ist der richtige Weg. Sie können den Basisklassenkonstruktor mithilfe einer Metaklasse effektiv überladen, sodass beim Aufrufen der Basisklasse das entsprechende Unterklassenobjekt erstellt wird. Im Folgenden finden Sie einen Beispielcode, der das Wesentliche meiner Bedeutung veranschaulicht.

Aktualisiert

Der Code wurde so geändert, dass er sowohl unter Python 2 als auch unter Python 3 ausgeführt wird, damit er relevant bleibt. Dies wurde so durchgeführt, dass die explizite Metaklassensyntax von Python vermieden wird, die zwischen den beiden Versionen variiert.

Um dieses Ziel zu erreichen, wird eine BulletMetaBaseInstanz der BulletMetaKlasse erstellt, indem die Metaklasse beim Erstellen der BulletBasisklasse explizit aufgerufen wird (anstatt das __metaclass__=Klassenattribut zu verwenden oder über ein metaclassSchlüsselwortargument abhängig von der Python-Version).

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')

Ausgabe:

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
quelle
Hmm, dies ist immer noch eine ausgefallene Möglichkeit, die Funktionen als add_bullet1, add_bullet2 usw. zu bezeichnen.
Bullets
1
@Bullets: Vielleicht ist es das, oder vielleicht ist es nur eine etwas aufwändige Möglichkeit, eine Factory-Funktion zu erstellen. Das Schöne daran ist, dass es eine Hierarchie von BulletUnterklassen unterstützt, ohne dass die Basisklasse oder die Factory-Funktion jedes Mal geändert werden muss, wenn Sie einen anderen Subtyp hinzufügen. (Wenn Sie C anstelle von C ++ verwenden, haben Sie wahrscheinlich keine Klassen.) Sie können auch eine intelligentere Metaklasse erstellen, die selbst herausfindet, welche Unterklasse basierend auf dem Typ und / oder der Nummer erstellt werden soll von übergebenen Argumenten (wie C ++ zur Unterstützung der Überladung).
Martineau
1
Diese Vererbungsidee wäre auch meine erste Option.
Daniel Möller
3

Python 3.8 hat functools.singledispatchmethod hinzugefügt

Transformieren Sie eine Methode in eine generische Single-Dispatch-Funktion.

Um eine generische Methode zu definieren, dekorieren Sie sie mit dem Dekorator @singledispatchmethod. Beachten Sie, dass der Versand für den Typ des ersten Nicht-Selbst- oder Nicht-CLS-Arguments erfolgt. Erstellen Sie Ihre Funktion entsprechend:

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=}")

Ausgabe

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

@singledispatchmethod unterstützt das Verschachteln mit anderen Dekoratoren wie @classmethod. Beachten Sie, dass die Einzelpatch-Methode der äußerste Dekorateur sein muss, um dispatcher.register zuzulassen. Hier ist die Negator-Klasse, wobei die neg-Methoden klassengebunden sind:

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=}")

Ausgabe:

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

Das gleiche Muster kann für andere ähnliche Dekorateure verwendet werden: statische Methode, abstrakte Methode und andere.

Vlad Bezden
quelle
2

Verwenden Sie Schlüsselwortargumente mit Standardwerten. Z.B

def add_bullet(sprite, start=default, direction=default, script=default, speed=default):

Im Fall einer geraden Kugel im Vergleich zu einer gekrümmten Kugel würde ich zwei Funktionen hinzufügen: add_bullet_straightund add_bullet_curved.

Rafe Kettler
quelle
2

Überladungsmethoden sind in Python schwierig. Es kann jedoch sinnvoll sein, das Diktat, die Liste oder die primitiven Variablen zu übergeben.

Ich habe etwas für meine Anwendungsfälle ausprobiert. Dies könnte hier helfen, die Leute zu verstehen, die die Methoden überladen.

Nehmen wir Ihr Beispiel:

Eine Klassenüberladungsmethode mit Aufruf der Methoden aus verschiedenen Klassen.

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

Übergeben Sie die Argumente der Remote-Klasse:

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

ODER

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

Daher wird die Behandlung für Listen-, Wörterbuch- oder primitive Variablen durch Überladen von Methoden erreicht.

Probieren Sie es für Ihre Codes aus.

shashankS
quelle
2

Nur ein einfacher Dekorateur

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)

Sie können es so verwenden

@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)

Ändern Sie es, um es an Ihren Anwendungsfall anzupassen.

Eine Klärung der Konzepte

  • Funktionsversand : Es gibt mehrere Funktionen mit demselben Namen. Welches sollte genannt werden? zwei Strategien
  • statischer Versand / Versand zur Kompilierungszeit (auch bekannt als "Überladung" ). Entscheiden Sie anhand des Kompilierungszeittyps der Argumente , welche Funktion aufgerufen werden soll . In allen dynamischen Sprachen gibt es keinen Typ zur Kompilierungszeit, sodass eine Überladung per Definition nicht möglich ist
  • Dynamischer Versand / Laufzeitversand : Entscheiden Sie anhand des Laufzeittyps der Argumente , welche Funktion aufgerufen werden soll . Dies ist, was alle OOP-Sprachen tun: Mehrere Klassen haben die gleichen Methoden, und die Sprache entscheidet anhand des self/thisArgumenttyps , welche aufgerufen werden soll . Die meisten Sprachen tun dies jedoch nur für das thisArgument. Der obige Dekorateur erweitert die Idee auf mehrere Parameter.

Nehmen Sie zum Aufräumen eine statische Sprache an und definieren Sie die Funktionen

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)

Beim statischen Versand (Überladen) wird zweimal "Nummer angerufen" angezeigt, da dies xals deklariert wurde Number, und das ist alles, was das Überladen betrifft. Beim dynamischen Versand wird "Ganzzahl aufgerufen, Float aufgerufen" angezeigt, da dies die tatsächlichen Typen xzum Zeitpunkt des Funktionsaufrufs sind.

blue_note
quelle
Dieses Beispiel zeigt entscheidend nicht, welche Methode xfür den dynamischen Versand aufgerufen wurde und in welcher Reihenfolge beide Methoden für den statischen Versand aufgerufen wurden. Empfehlen Sie, die print('number called for Integer')
Druckanweisungen