Dekoratory część 2
Mała rekapitulacja
Nasze poprzednie spotkanie z dekoratorami zakończyliśmy niemiłym akcentem. Nasz zrobiony najprostszym sposobem cache działał co prawda i tworzone przez niego obiekty robiły to, co od nich oczekiwaliśmy, jednak nie przebiegało to wszystko całkowicie bezszwowo. Udekorowana funkcja nazywała się inaczej i posiadała inną sygnaturę (zbiór argumentów, który według dokumentacji przyjmowała). Dodatkowo gdybyśmy spróbowali ją udokumentować, okazałoby się, że również jej docstring nie byłby zachowany.
Kto nam pomoże?
Rzeczywiście zadbanie o to wszystko przy budowie każdego dekoratora to dużo roboty. Skoro jednak robota ta jest w gruncie rzeczy powtarzalna i identyczna dla każdego dobrze sprawującego się dekoratora, to możemy się spodziewać, że ktoś już za nią ją odwalił. Tym kimś byli twórcy biblioteki decorator zawierającej dekorator @decorator.
Napisanie ostatniego zdania sprawiło mi, muszę się przyznać dużą frajdę. Jeśli jednak nadal będę w ten sposób opisywał funkcjonalność biblioteki, zrozumiałość wpisu zbliży się do zera. Dlatego przejdę od razu do przykładu:
from decorator import decorator from time import sleep def cache_caller(fun, *args): if fun.cache.has_key(args): return fun.cache[args] else: result = fun(*args) fun.cache[args] = result return result def cache(fun): fun.cache = {} return decorator(cache_caller, fun) @cache def dummy(x): """These goggles do nothing""" sleep(1) return x
Stworzona przez powyższy kod funkcja dummy po wywołaniu będzie zachowywać się będzie tak samo, jak jej odpowiednik z poprzedniego wpisu. Różnicę jednak zauważymy, gdy wywołamy polecenie help(dummy). Temu helpowi nie możemy już niczego zarzucić: funkcja ma taką nazwę, z jaką ją zdefiniowaliśmy, przyjmuje takie same argumenty (ma tę samą sygnaturę) i zachowuje docstring. O wszystko to zadbała biblioteka.
Napisałem powyżej, że funkcja decorator jest dekoratorem. W przykładzie powyższym jednak tego nie widzimy. Nie korzystamy tam ze składni z @, a funkcję wywołujemy z dwoma argumentami. Pierwszy to caller, czyli funkcja, która wywoła nam funkcję udekorowaną. Będzie on wywoływany za każdym razem, gdy będziemy chcieli wywołać funkcję dummy. Drugi to wrapper, który czyni z naszej funkcji dekorator. Ten wywołany zostanie tylko raz dla każdej dekorowanej funkcji. To w nim zawarte jest przypisanie do funkcji pustego słownika cache.
decorator jako dekorator
Możliwość korzystania z funkcji decorator, jako dekoratora widzimy wtedy, gdy wrapper jest trywialny i nie zawiera żadnej logiki oprócz "udekorowania" funckji. Załóżmy, że mamy grupę funkcji "niebezpiecznych". Przed wywołaniem każdej z nich chcemy zapytać użytkownika o pozwolenie. Według schematu powyżej wyglądałoby to tak:
from decorator import decorator def confirm_caller(fun, *args, **kwargs): prompt = 'Próbujesz wywołać funckję %s. Czy jesteś pewien (t/n): ' %\ fun.func_name while True: ans = raw_input(prompt) if ans.lower() in ['t', 'n']: break prompt = 'Niepoprawna odpowiedź! ' if ans.lower() == 't': return fun(*args, **kwargs) else: print "Odwołano" def confirm(fun): return decorator(confirm_caller, fun) @confirm def dangerous(): print "Zepsułeś komputer!!!" if __name__ == '__main__': dangerous()
Wywołanie funkcji dangerous spowoduje wyświetlenie ostrzeżenia i tylko jeśli użytkownik potwierdzi, jej ciało zostanie wykonane. Zwróćmy jednak uwagę na wrappera funkcji. Praktycznie identyczny będzie on w każdym przypadku, gdy nie ma żadnej dodatkowej logiki dla tworzeniu zmodyfikowanej funkcji (a tylko przy jej wywoływaniu). Dlatego biblioteka oferuje nam prostszy sposób osiągnięcia tego samego. Wystarczy:
- Usunąć z powyższego kodu funkcję confirm
- Przemianować callera tak, by nazywał się jak żądany dekorator (czyli w naszym przypadku confirm)
- Użyć funkcji decorator jako dekoratora dla confirm.
Po tej zmianie kod działa identycznie, a udało nam się pozbyć trywialnej i utrudniającej czytanie funkcji.
Podsumowując
Biblioteka decorator znacznie ułatwia nam tworzenie nowych dekoratorów przez:
- Zastąpienie notacji zagnieżdżonej notacją płaską.
- Zachowanie wszystkich cech dekorowanej funkcji takich jak docstring, sygnatura i nazwa.
Funkcję decorator wywołać możemy:
- Z dwoma argumentami, z których pierwszy to caller (funkcja wywołująca dekorowaną i zwracająca jej (być może zmieniony) wynik), a drugim funkcja wrappowana. Jeśli napiszemy funkcję wrapper przyjmującą funkcję jako argument i wołającą dla niej funkcję decorator, to taki wrapper może być użyty jako dekorator.
- Jako dekorator dla callera. Wówczas trywialny wrapper zostanie dla nas wygenerowany, a dekorator, który uzyskamy będzie nazywał się tak jak caller.
Ostatni wpis o dekoratorach będzie krótki. Powiem trochę o dekoratorach z dodatkowymi argumentami, dekorowaniu metod i o tym, na co należy uważać.