Ubacivanje metoda u postojeće Python objekte
U objektno-orjentiranim jezicima korištenje API-ja se često izvodi tako da postoji bazna klasa koja implementira neku funkcionalnost ali sadrži i dummy metode koje u baznoj klasi ne rade ništa nego služe kao hookovi koje podklasa može koristiti da bi se integrirala sa radom bazne klase.
Tipičan primjer ovoga su callbackovi. Ukoliko podklasa želi instalirati callback za neki događaj (npr lijevi klik mišem nad labelom) može overloadati metodu koja se poziva na taj događaj (a koja u baznoj klasi ne radi ništa).
U PyQT-u (sintaksa ide po sjećanju) to izgleda otprilike ovako:
class MyLabel(QLabel):
def onKeyPress(self, event):
...do something...
Dva problema s ovim su: ukoliko želimo različitu funkcionalnost u keypress eventu za nekoliko objekata, moramo kreirati nekoliko različitih podklasa; a ukoliko netko drugi kreira objekt, ponekad ne možemo odrediti objekt koje klase treba instancirati.
Tipičan pattern za rješavanje potonjeg problema je Factory, odnosno klasa koja opisuje koju klasu želite instancirati. Ali, kako je Python dinamički jezik, imamo i alternativu – monkey-patch.
Monkeypatchiranje je tehnika mijenjanja ponašanja već postojećeg koda prilikom izvršavanja. Stvar je prilično moćna i lako se upucati u nogu, pa se obično ne preporuča ukoliko postoje bolji/elegantniji načini za rješavanje problema. Ali ovdje će nam izvrsno poslužiti da bi modificirali ponašanje već instanciranog objekta.
Mali snippet Python koda s kojim ćemo raditi:
class Foo(object):
def foo(self):
print "foo called"
def bar(self):
print "bar called"
x = Foo()
x.bar() # ispisati će "bar called"
Ideja nam je overrideati metodu bar da ispiše nešto drugo:
class Bar(Foo):
def bar(self):
print "different bar called"
x = Bar()
x.bar() # ispisati će "different bar called"
Zasad nema iznenađenja, ovo je standardni OOP. Ali što ako već imamo definiran ‘x’ kao instancu od Foo? Prva stvar koju možemo napraviti je promjeniti mu klasu:
x = Foo() x.bar() # ispisuje "bar called" x.__class__ = Bar # mijenjamo klasu objektu x.bar() # ispisuje "different bar called"
Ako vam ovo djeluje suludo, dobrodošli u dinamičke jezike ;-)
Još uvijek imamo problem sa činjenicom da moramo imati definiciju klase Bar. Ukoliko samo želimo promjeniti jednu metodu, ne da nam se svaki put pisati klasu. U tom slučaju možemo napraviti slijedeće:
def baz(self):
print "yet another bar"
def add_method(obj, meth):
obj.__class__ = type('newclassname', (Foo,), { 'bar': meth })
add_method(x, baz)
x.bar() # ispisuje "yet another bar"
Ovdje smo u tijeku izvođenja kreirali novu klasu “baz” koja naslijeđuje klasu Foo a ima jednu metodu zvanu “bar”, i promjenili postojećeg objekta u novkreiranu klasu.
Kao što se vidi, ovime možemo raditi prilično moćne modifikacije već postojećeg koda prilikom samog izvršavanja. Ali ovaj specifičan slučaj možemo rješiti i nešto jednostavnije i elegantnije:
def baz(self): print "yet another bar" def add_method(obj, meth): setattr(obj, meth.__name__, lambda *args, **kwargs: meth(obj, *args, **kwargs)) add_method(x, baz) x.baz() # ispisuje "yet another bar"
Ovaj put nismo morali generirati novu jednokratnu klasu samo da bi dodali metodu, nego smo funkciju dodali kao atribut objekta. Dodanu funkciju Python ne tretira magično kao metodu – da smo je samo dodali kao takvu, Python bi je tretirao kao obični callable atribut i samo je pozvao, pa kao prvi argument ne bi bila proslijeđena referenca na sam objekt. Kako bi se funkcija pozvala sa prvim argumentom objektom, morali smo kreirati closure koji je boundao referencu na objekt i proslijedio je funkciji kao prvi argument (tome služi ona lambda).
Update: Popravio poziv funkcije u zadnjem snippetu iz “x.bar()” u “x.baz()”, thx @nikolaplejic na uočavanju greške. Usput sam pokušao i malo pojasniti zašto je potrebno wrappati metodu i ručno proslijediti referencu na objekt.
Update 2: Ideja u članku je dodati metodu pojedinoj instanci objekta, ne proširiti cijelu klasu. Ukoliko želimo prilikom izvođenja dodati (ili izmjeniti) metode u nekoj klasi (a da to odmah bude vidljivo svim trenutnim i budućim instancama te klase, možemo napraviti slijedeće:
def baz(self): print "yet another bar" x = Foo() x.bar() # ispisuje "bar called" Foo.bar = baz x = Foo() x.bar() # ispisuje "yet another bar"
U tom slučaju će se python čak pobrinuti oko automatskog bindanja metode instanci i proslijeđivanja reference kao prvog argumenta, i ovakav monkeypatching je relativno čest (iako još uvijek opasan ako se zloupotrebljava :) u Pyhtonu.
Ovo funkcionira, i dobro je da se u Pythonu ovako nešto može izvesti.
Ali već vidim kako ovo ekipa zloupotrebljava ovakvu tehniku. Ovo je inače dosta netrasparentno što se software dizjana tiče. Sličnu tehniku sam ja koristio u Java scritpu (no da bi dobio polimorfizam, kojeg po defaultu nema u JS), no takovo dodavanje metoda u razred je vrlo ne transparento i netko se može kasnije pitati od kud ova metoda ovdje. Kao što si i sam rekao, Netko ti je ovim dao automatsku pušku, a sad je na programeu da se ne upuca u nogu.
Yep, slažem se, kod korištenja ovakvih stvari treba imati vrlo dobre razloge zašto to a ne nešto drugačije / standardnije.
Ali dobro je znati da imaš automatsku pušku ako ti zatreba :-)
Od samog objašnjenja KAKO koncept izvesti, više bi smisla imalo objasniti GDJE koncept primijeniti i koji se problemi mogu na taj (i jedino na taj?!) način riješiti.
zakon post ;)
@slavus mi smo upali u ovu temu iz use case scenarija gdje gradis python aplikaciju u interaktivnom shellu (ipython), pa je super imati mogucnost naknadno modificirati instance… npr. proucavajuci dokumentaciju i gradnjom prvih prototipova u proucavanju nekog novog frameworka….
Nekad su ovo proglašavali problemom i sigurnosnim rizikom, opasnošću od code injectiona i sličnog. Pa je Python okrenuo pilu naopako i pretvorio to u feature :)
Mijenjanje klase na već kreiranom objektu ne bi nikome preporučio. Ostale tehnike “monkey patching-a” (dinamičko kreiranje klase, dodavanje/promjena metode u runtime, generiranje koda, …), isto nisu za preporuku.
To što Python to omogućava, ne znači da se to treba koristiti (mogu se u Pythonu i puno “gore” stvari raditi).
Slažem se s pristupom iz jednog komentara, da je važno vidjeti zašto se to radi, tj. kad to primjeniti. Zato bi bilo dobro da objasniš zašto jedan objekt mora glumiti desetak klasa u runtime-u i zašto baš na ovaj način?
Python nudi mnoštvo tehnika za postizanje dinamičkih svojstava u runtime-u koje su puno transparentnije ( __getattr__, __setattr__ u inicijalizaciji objekta, ili inicijalizaciji klase / koristiti __metaclass__, …), gdje svaka od njih ima svoju specifičnu namjenu i prednosti.
Osim što meni osobno MP izgleda “ružno” (doduše i moćno), mislim da se sukobljava s filozofijom Python-a, tj. pep-20 (izdvojeno):
>>> import this
The Zen of Python, by Tim Peters
Explicit is better than implicit.
There should be one– and preferably only one –obvious way to do it.
Special cases aren’t special enough to break the rules.
In the face of ambiguity, refuse the temptation to guess.
Beautiful is better than ugly.
Simple is better than complex.
Flat is better than nested.
If the implementation is hard to explain, it’s a bad idea.
@Robert: Slažem se da je monkeypatching opasan, ne-pythonic ali i vrlo moćan (Ruby ekipa se dosta navukla na monkeypatching pa sad jadikuju).
Osobno sam ga koristio par puta za krpanje manjih grešaka u system-wide instaliranim modulima koje nisam želio/mogao forkati (u smislu, promjenio definiciju neke funkcije u postojećoj klasi umjesto da radim podklasu i mijenjam ostatak svog koda kako bi koristio moju podklasu i zaobilazio grešku).
Što se tiče promjene klase postojećim objektima, u postu jesam naveo da mi je više pythonic dodati metodu postojećoj instanci, nego mijenjati klasu te instance. U komentarima je Marcel već naveo konkretan slučaj kad je bilo potrebno mijenjanje metoda već instanciranog objekta.
Post je nastao iz diskusije proizašle iz tog problema, a učinilo mi se da bi bilo zgodno i široj javnosti pokazati što se sve može. IMHO, od viška znanja glava ne boli :)