## Več funkcij z istim imenom in različnimi argumenti

Študente pogosto zanima, ali je mogoče v Pythonu napisati več različic iste funkcije; katera od njih se pokliče, bi bilo odvisno od tipa argumenta. Eno funkcijo, torej, za argumente tipa `int` in drugo za sezname, ter mogoče še tretjo za množice. Inspiracija izvira iz jezikov v slogu C++, ki poznajo function overloading in funkcije

    int f(int x) {
        return x * 2;
    }

    int f(float x) {
        return x * 3;
    }

    inf f(int x, int y) {
        return x * y;
    }

pa bo klic `f(42)` vrnil `84`, klic `f(3.14)` pa `9.42`, če se prav spomnim C++-a. In klic `f(2, 3)` bo vrnil `6`.

Je v Pythonu torej to mogoče?

Ne. In da.

Python - kot jezik - tega nima. Pač pa je možno delati čudeže z dekoratorji.

### Dekorator `singledispatch`

    from functools import singledispatch

    @singledispatch
    def f(x):
        print(f"Takega argumenta, {x}, nihče ne mara.")

    @f.register
    def _(x: int):
        print("O, glej no, int")

    @f.register
    def _(a: float, b: str):
        print("Česa ne poveste, float smo dobili!")

    @f.register
    def _(s: set):
        print("Množica? Tudi to obstaja?")

    f(42)

    O, glej no, int

    f(3.14, "test")

    Česa ne poveste, float smo dobili!

    f({1, 2, 3})

    Množica? Tudi to obstaja?

    f([1, 2, 3])

    Takega argumenta, [1, 2, 3], nihče ne mara.

Reč ima omejitve. O tem, katera funkcija se bo poklicala, odloča le prvi element. Tip pa mora biti razred; napisali smo `set`, `set[int]` pa ne deluje.

Razvajencu iz C++ to seveda ni dovolj. V praksi pa tega ne potrebujemo ravno velikokrat (vsaj jaz to rekdo uporabljam - pa ne zato, ker mi ne bi bilo blizu, v C++ sem imel to rad!) in omejitev na en sam argument sploh ni tako huda.

To je vse. Naprej naj berejo tisti, ki jih zanima, kako je to narejeno.

### Parametrizirani dekoratorji

Bolj zaradi tega, kar sledi v naslednjem razdelku, povejmo, da gre tudi brez označevanje tipov argumentov. V tem primeru je potrebno tip podati dekoratorju `register`.

    from functools import singledispatch

    @singledispatch
    def f(x):
        print(f"Takega argumenta, {x}, nihče ne mara.")

    @f.register(int)
    def _(x):
        print("O, glej no, int")

    @f.register(float)
    def _(a, b):
        print("Česa ne poveste, float smo dobili!")

    @f.register(set)
    def _(s):
        print("Množica? Tudi to obstaja?")

### Kako je to narejeno

Napisal sem, da tega ne omogoča Python kot jezik, temveč je to možno narediti z dekoratorjem. Z drugimi besedami: če tega ne bi bilo, bi lahko to naredili sami. Ker je preprosteje (kar še ne pomeni, da je preprosto), pokažimo, kako bi sami naredili to, drugo, parametrizirano različico dekoratorja.

    def moj_dispatch(f):
        funkcije = {}
        def g(prvi, *ostali):
            return funkcije.get(type(prvi), f)(prvi, *ostali)

        def register(tip):
            def registriraj(f):
                funkcije[tip] = f
            return registriraj

        g.register = register
        return g

Za začetek razlage se spomnimo, kako bomo uporabili dekorator:

    @moj_dispatch
    def f(x):
        print(f"Takega argumenta, {x}, nihče ne mara.")

`moj_dispatch` je dekorator, ki pripravi slovar, v katerega bo shranjeval registrirane funkcije (ključ je tip, vrednost je funkcija, ki prejme argument tega tipa). Nato sestavi funkcijo `g`, ki jo bo kasneje tudi vrnil - funkcija `f` (iz primera uporabe) bo v resnici ta `g`. Funkcija `g` bo prejela nek argument (enega ali več - ostali so v `*ostali`). V slovarju poišče funkcijo, ki ustreza tipu `x`; če take funkcije ni, kot privzeto vrednost uporabi podani `f`; zato torej `funkcije.get(type(prvi), f)`. To funkcijo pokliče z argumenti `x` in `*ostali`-mi ter vrne njen rezultat.

Potem pa se spomnimo, kako bomo registrirali nove funkcije:

    @f.register(int)
    def _(x):
        print("O, glej no, int")

Da bo to delovalo, mora imeti `f` metodo, atribut ali kakorkoli hočemo temu reči `register`. Na srečo lahko Pythonovim funkcijam prirejamo atribute; za `f.register` poskrbimo z `g.register = register`. Seveda je potrebno `register` prej definirati.

Na prvi pogled bo `f.register` dekorator. To je zmota. `f.register` pokličemo s tipom. `f.register` torej ni dekorator, pač pa mora `f.register` vrniti dekorator. Drži? Razumemo? V gornji kodi piše `@f.register(int)`, torej je `f.register(int)` dekorator. `register` mora vrniti dekorator, z drugimi besedami, `f.register` mora vrniti funkcijo, ki prejme neko funkcijo (v našem primeru funkcijo `_`) in jo "dekorira". Kako jo dekorira? Pravzaprav ... zelo slabo. Vse kar naredi, je, da jo shrani v slovar. `register(int)` je tako zanič dekorator, da sploh ničesar ne vrne! Po tem "dekoriranju" `_` sploh ni funkcija temveč `None`. Vendar nas to ne moti, saj je ne bomo nikoli direktno klicali.

To je vse.

    @moj_dispatch
    def f(x):
        print(f"Takega argumenta, {x}, nihče ne mara.")

    @f.register(int)
    def _(x):
        print("O, glej no, int")

    @f.register(float)
    def _(a, b):
        print("Česa ne poveste, float smo dobili!")

    @f.register(set)
    def _(s):
        print("Množica? Tudi to obstaja?")

    f(42)

    O, glej no, int

    f(3.14, "test")

    Česa ne poveste, float smo dobili!

    f({1, 2, 3})

    Množica? Tudi to obstaja?

V resnici je `singledispatch` seveda malo bolj zapleten. Med drugim, recimo, dekorator `register` vrača funkcijo,

        def register(tip):
            def registriraj(f):
                funkcije[tip] = f
                return f
            return registriraj

kar omogoča, da isto funkcijo uporabimo za več tipov,

    @f.register(int)
    @f.register(float)
    def _(x):
        print("Prejeli smo int ali float")

Še več komplikacij povzroči to, da lahko uporabljamo `f.register` s parametrom (npr. `int`) ali brez.

A osnovni princip je pravilen; naš dekorator deluje.

### Kaj lahko poskusite

Če razumete gornjo kodo, napišite dekorator, ki bo deloval za več argumentov: če navedemo, recimo, tri tipe

    @f.register(int, int, float)
    def _(x, y, z, w):
        pass

uporabi za izbor funkcije tipe prvih treh spremenljivk.