Przeciążenie funkcji Pythona


213

Wiem, że Python nie obsługuje przeciążania metod, ale napotkałem problem, którego nie potrafię rozwiązać w przyjemny Python.

Tworzę grę, w której postać musi strzelać różnymi kulami, ale jak napisać różne funkcje do tworzenia tych kul? Załóżmy na przykład, że mam funkcję, która tworzy pocisk przemieszczający się z punktu A do B z określoną prędkością. Napiszę taką funkcję:

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

Ale chcę napisać inne funkcje do tworzenia pocisków, takie jak:

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

I tak z wieloma odmianami. Czy jest lepszy sposób, aby to zrobić bez użycia tak wielu argumentów słowa kluczowego, że robi się trochę brzydko szybko. Zmiana nazwy poszczególnych funkcji jest bardzo złe zbyt, ponieważ można uzyskać albo add_bullet1, add_bullet2albo add_bullet_with_really_long_name.

Aby odpowiedzieć na niektóre odpowiedzi:

  1. Nie, nie mogę utworzyć hierarchii klas Bullet, ponieważ jest to zbyt wolne. Rzeczywisty kod do zarządzania punktorami znajduje się w C, a moje funkcje są otoczone przez C API.

  2. Wiem o argumentach słów kluczowych, ale sprawdzanie wszelkiego rodzaju kombinacji parametrów staje się denerwujące, ale domyślne argumenty pomagają przydzielić acceleration=0


5
Działa tylko z jednym parametrem, ale tutaj (dla osób przychodzących tutaj z wyszukiwarki): docs.python.org/3/library/…
leewz

1
wydaje się, że to dobre miejsce na wartości domyślne. możesz ustawić niektóre na Brak i po prostu je sprawdzić. dodatkowy wpływ boolowski wydaje się nieistotny
Andrew Scott Evans

Muszą używać default value + if + elsetego samego co C ++. Jest to jedna z niewielu rzeczy, które C ++ ma lepszą czytelność niż Python ...
Deqing

Nie jestem pewien, dlaczego kwargs nie jest prawidłową odpowiedzią. Mówisz, że nie chcesz używać wielu argumentów słów kluczowych, ponieważ robi się brzydko szybko ... no właśnie taka jest natura problemu. Jeśli masz wiele argumentów i jest bałagan, ponieważ masz wiele argumentów, niż się spodziewałeś? Czy chcesz użyć wielu argumentów bez podawania ich gdziekolwiek? Python nie jest czytnikiem myśli.
Rachunek

Nie wiemy, jakie to obiekty script, curve, czy mają wspólnego przodka, jakie metody obsługują. W przypadku pisania typu „kaczor” to od Ciebie zależy, czy projekt klasowy obliczy, jakie metody muszą obsługiwać. Przypuszczalnie Scriptobsługuje pewnego rodzaju wywołanie zwrotne oparte na taktowaniu czasu (ale jaki obiekt powinien zwrócić? Pozycję w tym czasie? Trajektorię w tym czasie?). Prawdopodobnie start, direction, speedi start, headto, spead, accelerationoba opisują rodzaje trajektorii, ale znowu to od Ciebie zależy zaprojektowanie klasy odbierającej, aby wiedziała, jak ją rozpakować i przetworzyć.
smci

Odpowiedzi:


144

To, o co prosisz, nazywa się wysyłką wielokrotną . Zobacz przykłady języków Julii , które pokazują różne rodzaje wysyłek.

Jednak zanim się na to spojrzymy, najpierw zastanowimy się, dlaczego przeciążenie nie jest tak naprawdę tym, czego chcesz w Pythonie.

Dlaczego nie przeciążenie?

Po pierwsze, należy zrozumieć pojęcie przeciążenia i dlaczego nie ma ono zastosowania do Pythona.

Podczas pracy z językami, które mogą dyskryminować typy danych w czasie kompilacji, wybór opcji może wystąpić w czasie kompilacji. Czynność tworzenia takich alternatywnych funkcji do wyboru w czasie kompilacji jest zwykle nazywana przeciążeniem funkcji. ( Wikipedia )

Python jest językiem dynamicznie typowanym, więc koncepcja przeciążenia po prostu go nie dotyczy. Jednak nie wszystko stracone, ponieważ możemy tworzyć takie alternatywne funkcje w czasie wykonywania:

W językach programowania, które odraczają identyfikację typu danych do czasu wykonania, wybór między funkcjami alternatywnymi musi nastąpić w czasie wykonywania, na podstawie dynamicznie określonych typów argumentów funkcji. Funkcje, których alternatywne implementacje są wybierane w ten sposób, są ogólnie nazywane multimetodami . ( Wikipedia )

Powinniśmy więc być w stanie wykonywać multimetody w Pythonie - lub, jak to się nazywa alternatywnie: wielokrotne wysyłanie .

Wielokrotna wysyłka

Multimetody są również nazywane wysyłaniem wielokrotnym :

Wielokrotne wysyłanie lub multimetody to cecha niektórych obiektowych języków programowania, w których funkcja lub metoda może być dynamicznie wysyłana na podstawie typu wykonania (dynamicznego) więcej niż jednego z jej argumentów. ( Wikipedia )

Python nie obsługuje tego po wyjęciu z pudełka 1 , ale tak się składa, że ​​istnieje doskonały pakiet Pythona o nazwie multipledispatch , który właśnie to robi.

Rozwiązanie

Oto, w jaki sposób możemy wykorzystać pakiet multipledispatch 2 do wdrożenia twoich metod:

>>> 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 obsługuje obecnie pojedynczą wysyłkę.
2. Uważaj, aby nie używać multipledispatch w środowisku wielowątkowym, bo dostaniesz dziwne zachowanie.


6
Na czym polega problem z „multipledispatch” w środowisku wielowątkowym? Ponieważ kod po stronie serwera jest zwykle w środowisku wielowątkowym! Próbuję to wykopać!
danzeer

7
@danzeer To nie było bezpieczne dla wątków. Widziałem, jak argument jest modyfikowany przez dwa różne wątki (tj. Wartość speedmoże zmienić się w środku funkcji, gdy inny wątek ustawia własną wartość speed) !!! Długo mi zajęło uświadomienie sobie, że winowajcą była biblioteka.
Andriy Drozdyuk

108

Python obsługuje „przeciążanie metod” w trakcie prezentacji. W rzeczywistości to, co właśnie opisujesz, jest trywialne do zaimplementowania w Pythonie na tak wiele różnych sposobów, ale wybrałbym:

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

W powyższym kodzie defaultjest prawdopodobną wartością domyślną dla tych argumentów lub None. Następnie możesz wywołać metodę tylko z argumentami, które Cię interesują, a Python użyje wartości domyślnych.

Możesz także zrobić coś takiego:

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

Inną alternatywą jest bezpośrednie połączenie żądanej funkcji bezpośrednio z klasą lub instancją:

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

Jeszcze innym sposobem jest użycie abstrakcyjnego wzorca fabrycznego:

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 

107
Wszystkie te wyglądają jak przykłady zmiennych argumentów, a nie przeciążenie. Ponieważ przeciążenie pozwala mieć tę samą funkcję, dla różnych typów jako argumentów. np .: sum (real_num1, real_num2) i sum (imaginary_num1, imaginary_num2) Oba będą miały tę samą składnię wywoływania, ale w rzeczywistości oczekują 2 różnych typów danych wejściowych, a implementacja musi się również zmienić wewnętrznie
Efren

17
Korzystając z odpowiedzi, którą wybierzesz, w jaki sposób przedstawisz rozmówcy, które argumenty mają sens razem? Po prostu umieszczenie kilku argumentów, z których każdy ma domyślną wartość, może zapewnić tę samą funkcjonalność, ale pod względem interfejsu API jest znacznie mniej elegancki
Greg Ennis

6
Nie jest to przeciążenie, implementacja będzie musiała sprawdzić wszystkie kombinacje danych wejściowych parametrów (lub zignorować parametry), takie jak: if sprite and script and not start and not direction and not speed...po prostu wiedzieć, że jest w określonej akcji. ponieważ dzwoniący może wywołać funkcję, podając wszystkie dostępne parametry. Podczas przeciążania określ dla Ciebie dokładne zestawy odpowiednich parametrów.
Roee Gavirel

5
To bardzo denerwujące, gdy ludzie mówią, że Python obsługuje przeciążanie metod. To nie. Fakt, że w cudzysłowie umieściłeś „przeciążenie metody”, świadczy o tym. Możesz uzyskać podobną funkcjonalność za pomocą kilku technik, takich jak ta wymieniona tutaj. Ale przeciążanie metod ma bardzo konkretną definicję.
Howard Swope

Myślę, że zamierzonym punktem jest to, że podczas gdy przeciążanie metod nie jest cechą Pythona, powyższe mechanizmy można wykorzystać do osiągnięcia równoważnego efektu.
rawr zadzwonił

93

Możesz użyć rozwiązania „roll-your-own” do przeciążania funkcji. Ten jest skopiowany z artykułu Guido van Rossuma o multimetodach (ponieważ istnieje niewielka różnica między mm a przeciążeniem w pythonie):

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

Wykorzystanie byłoby

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

Obecnie najbardziej restrykcyjnymi ograniczeniami są:

  • metody nie są obsługiwane, tylko funkcje, które nie są członkami klasy;
  • dziedziczenie nie jest obsługiwane;
  • kwargs nie są obsługiwane;
  • rejestracja nowych funkcji powinna odbywać się w czasie importu, rzecz nie jest wątkowo bezpieczna

6
+1 dla dekoratorów za rozszerzenie języka w tym przypadku użycia.
Eloims

1
+1, ponieważ jest to świetny pomysł (i prawdopodobnie to, z czym powinien iść PO) --- Nigdy nie widziałem implementacji multimodalnej w Pythonie.
Escualo

39

Możliwą opcją jest użycie modułu multipledispatch, jak opisano tutaj: http://matthewrocklin.com/blog/work/2014/02/25/Multiple-Dispatch

Zamiast tego:

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

Możesz to zrobić:

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)

W wyniku użycia:

>>> add(1, 2)
3

>>> add(1, 'hello')
'1 + hello'

4
Dlaczego nie dostaje to więcej głosów? Zgaduję z powodu braku przykładów ... Stworzyłem odpowiedź z przykładem, jak zaimplementować rozwiązanie problemu OP z pakietem multipledispatch .
Andriy Drozdyuk

19

W Pythonie 3.4 dodano PEP-0443. Funkcje ogólne pojedynczej wysyłki .

Oto krótki opis API z PEP.

Aby zdefiniować funkcję ogólną, udekoruj ją dekoratorem @singledispatch. Zauważ, że wysyłka odbywa się na podstawie pierwszego argumentu. Utwórz odpowiednio swoją funkcję:

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

Aby dodać przeciążone implementacje do funkcji, użyj atrybutu register () funkcji ogólnej. To jest dekorator, przyjmujący parametr typu i dekorujący funkcję realizującą operację dla tego typu:

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

11

Ten typ zachowania zazwyczaj rozwiązuje się (w językach OOP) za pomocą polimorfizmu. Każdy rodzaj pocisku byłby odpowiedzialny za wiedzę o tym, jak się porusza. Na przykład:

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

Przekaż tyle argumentów do istniejącej funkcji c_, a następnie wykonaj zadanie określenia, która funkcja c ma zostać wywołana na podstawie wartości w początkowej funkcji c. Python powinien więc zawsze wywoływać tylko jedną funkcję c. Ta jedna funkcja c sprawdza argumenty, a następnie może odpowiednio delegować na inne funkcje c.

Zasadniczo używasz każdej podklasy jako innego kontenera danych, ale definiując wszystkie potencjalne argumenty klasy podstawowej, podklasy mogą zignorować te, z którymi nic nie robią.

Kiedy pojawia się nowy typ pocisku, możesz po prostu zdefiniować jeszcze jedną właściwość w bazie, zmienić funkcję jednego pytona, aby przekazywała dodatkową właściwość, i jedną funkcję c_funkcji, która odpowiednio analizuje argumenty i deleguje. Chyba nie brzmi to tak źle.


1
To było moje początkowe podejście, ale ze względu na wydajność musiałem przepisać ten kod w C.
Bullets

@Bullety, sugerowałbym, że może być dostępnych wiele różnych opcji poprawiających wydajność zamiast pisania całej liczby funkcji c, które prawdopodobnie nie będą zbyt wiele robić. Na przykład: utworzenie instancji może być kosztowne, więc utrzymuj pulę obiektów. Chociaż mówię to, nie wiedząc, co okazało się zbyt wolne. Co było takiego wolnego od zainteresowania, co nie było interesujące? O ile znaczna ilość czasu nie zostanie poświęcona po stronie C granicy, nie sądzę, że Python (sam) jest prawdziwym problemem.
Josh Smeaton

Być może istnieją inne sposoby poprawy wydajności, ale jestem znacznie lepszy z C niż z Pythonem. Problem polegał na obliczaniu ruchów pocisków i wykrywaniu, kiedy wypadają one poza granice ekranu. Miałem metody obliczania położenia kuli, pos+v*ta następnie porównywania do granic ekranu if x > 800i tak dalej. Wywołanie tych funkcji kilkaset razy na klatkę okazało się niedopuszczalnie wolne. Było to coś w rodzaju 40 fps przy 100% procesora z czystym pytonem do 60 fps z 5% -10% po wykonaniu w C.
Bullets

@Bullety, w takim razie dość uczciwe. Nadal używałbym podejścia, z którego korzystałem, do enkapsulacji danych. Przekaż wystąpienie pocisku add_bulleti wyodrębnij wszystkie potrzebne pola. Zmienię swoją odpowiedź.
Josh Smeaton,

@Bullety: Możesz połączyć swoje funkcje C i podejście OOP sugerowane przez Josha za pomocą Cython . Umożliwia wcześniejsze wiązanie, więc nie powinno być kary prędkości.
jfs


4

Użyj w definicji wielu argumentów słów kluczowych lub utwórz Bullethierarchię, której instancje są przekazywane do funkcji.


Chciałem zasugerować drugie podejście: stworzyć klasy BulletParams ..., aby określić szczegóły pocisku.
John Zwinck

Czy możesz to rozwinąć? Próbowałem utworzyć hierarchię klas z różnymi punktorami, ale to nie działa, ponieważ Python jest zbyt wolny. Nie potrafi wystarczająco szybko obliczyć ruchów wymaganej liczby pocisków, więc musiałem napisać tę część w C. Wszystkie warianty add_bullet wywołują odpowiednią funkcję C.
Bullets

4

Myślę, że twoim podstawowym wymaganiem jest posiadanie składni podobnej do C / C ++ w pythonie przy możliwie najmniejszym bólu głowy. Chociaż podobała mi się odpowiedź Aleksandra Poluektowa, nie działa na zajęciach.

Dla klas powinny działać następujące elementy. Działa, rozróżniając liczbę argumentów niebędących słowami kluczowymi (ale nie obsługuje rozróżniania według typu):

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

I można go użyć w następujący sposób:

test = TestOverloading()

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

Wynik:

To przeciążenie. 3
Sprite: I'm a Sprite
Start: 0
Kierunek: Racja

To przeciążenie 2
Sprite: Jestem innym
skryptem Sprite :
podczas gdy x == Prawda: print 'hi'



3

Myślę, że droga do Bullethierarchii klas z powiązanym polimorfizmem jest słuszna. Można skutecznie przeciążyć konstruktora klasy bazowej za pomocą metaklasy, tak aby wywołanie klasy bazowej spowodowało utworzenie odpowiedniego obiektu podklasy. Poniżej znajduje się przykładowy kod ilustrujący istotę tego, co mam na myśli.

Zaktualizowano

Kod został zmodyfikowany, aby działał zarówno w Pythonie 2, jak i 3, aby był odpowiedni. Dokonano tego w sposób, który pozwala uniknąć użycia jawnej składni metaklasy Pythona, która różni się między dwiema wersjami.

Aby osiągnąć ten cel, BulletMetaBaseinstancja BulletMetaklasy jest tworzona przez jawne wywołanie metaklasy podczas tworzenia klasy podstawowej Bullet(zamiast używania __metaclass__=atrybutu klasy lub metaclassargumentu słowa kluczowego w zależności od wersji Pythona).

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

Wynik:

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

Hmm, to wciąż tylko fantazyjny sposób nazywania funkcji jako add_bullet1, add_bullet2 i tak dalej.
Bullets,

1
@Bullets: Być może tak, a może to tylko nieco skomplikowany sposób na utworzenie funkcji fabrycznej. Zaletą jest to, że obsługuje hierarchięBullet podklas bez konieczności modyfikowania klasy podstawowej lub funkcji fabrycznej za każdym razem, gdy dodajesz inny podtyp. (Oczywiście, jeśli używasz C zamiast C ++, myślę, że nie masz klas.) Możesz również stworzyć mądrzejszą metaklasę, która samodzielnie wymyśliła, jaką podklasę utworzyć na podstawie typu i / lub liczby przekazanych argumentów (podobnie jak C ++ obsługuje przeciążanie).
martineau

1
Ten pomysł dziedziczenia byłby również moją pierwszą opcją.
Daniel Möller,

3

W Pythonie 3.8 dodano funkcję funkools.singledispatch

Przekształć metodę w funkcję ogólną pojedynczej wysyłki.

Aby zdefiniować metodę ogólną, udekoruj ją dekoratorem @singledispatchmethod. Zauważ, że wysyłka odbywa się według typu pierwszego argumentu innego niż self lub non-cls, utwórz odpowiednio swoją funkcję:

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

Wynik

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

@singledispatchmethod obsługuje zagnieżdżanie z innymi dekoratorami, takimi jak @classmethod. Należy pamiętać, że aby zezwolić na dispatcher.register, metoda singledispatch musi być najbardziej zewnętrznym dekoratorem. Oto klasa Negator, a metody neg są powiązane klasą:

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

Wynik:

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

Ten sam wzór można zastosować w przypadku innych podobnych dekoratorów: metoda statyczna, metoda abstrakcyjna i inne.


2

Używaj argumentów słów kluczowych z wartościami domyślnymi. Na przykład

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

W przypadku pocisku prostego kontra zakrzywionego dodałbym dwie funkcje: add_bullet_straighti add_bullet_curved.


2

przeciążanie metod jest trudne w Pythonie. Można jednak użyć przekazywania zmiennych, list lub prymitywnych zmiennych.

Próbowałem czegoś dla moich przypadków użycia, może to pomóc tutaj zrozumieć ludzi, którzy przeciążają metody.

Weźmy twój przykład:

metoda przeciążenia klasy z wywołaniem metod z innej klasy.

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

przekaż argumenty ze zdalnej klasy:

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

LUB

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

Obsługuje się więc obsługę list, słowników lub prymitywnych zmiennych z przeciążenia metody.

wypróbuj to dla swoich kodów.


2

Po prostu prosty dekorator

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)

Możesz użyć tego w ten sposób

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

Zmodyfikuj go, aby dostosować go do swojego przypadku użycia.

Wyjaśnienie pojęć

  • wysyłka funkcji : istnieje wiele funkcji o tej samej nazwie. Który należy nazwać? dwie strategie
  • wysyłka statyczna / w czasie kompilacji ( aka. „przeładowanie” ). zdecyduj, którą funkcję wywołać na podstawie typu argumentów w czasie kompilacji . We wszystkich dynamicznych językach nie ma typu czasu kompilacji, więc przeciążenie jest z definicji niemożliwe
  • dynamiczne / wysyłanie w czasie wykonywania : zdecyduj, którą funkcję wywołać na podstawie typu środowiska wykonawczego argumentów. Tak postępują wszystkie języki OOP: wiele klas ma te same metody, a język decyduje, który z nich wywołać na podstawie typu self/thisargumentu. Jednak większość języków robi to tylko dla thisargumentu. Powyższy dekorator rozszerza pomysł na wiele parametrów.

Aby wyczyścić, załóż język statyczny i zdefiniuj funkcje

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)

W przypadku wysyłki statycznej (przeciążenia) zobaczysz dwukrotnie „numer wywoływany”, ponieważ xzostał on zadeklarowany jako Numberi to wszystko oznacza przeciążenie. W przypadku dynamicznej wysyłki pojawi się „liczba całkowita nazywana, liczba zmiennoprzecinkowa”, ponieważ są to rzeczywiste typy xw momencie wywołania funkcji.


Ten przykład nie pokazuje, która metoda została wywołana xdla dynamicznej wysyłki, ani w jakiej kolejności obie metody zostały wywołane dla wysyłki statycznej. Polecam edytowanie drukowanych wyciągów print('number called for Integer')itp.
smci
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.