Š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;
}
(int x, int y) {
inf freturn 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.
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?")
42) f(
O, glej no, int
3.14, "test") f(
Česa ne poveste, float smo dobili!
1, 2, 3}) f({
Množica? Tudi to obstaja?
1, 2, 3]) f([
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.
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?")
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):
= f
funkcije[tip] return registriraj
= register
g.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?")
42) f(
O, glej no, int
3.14, "test") f(
Česa ne poveste, float smo dobili!
1, 2, 3}) f({
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):
= f
funkcije[tip] 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.
Č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.