I przychodzi czas na Dekoratory i udekorowanie naszego kodu 🙂 Tworząc sobie radośnie funkcje, i przestrzegając przy tym zasady DIY można natrafić na problem, gdy przykładowo mamy funkcję x. Chcemy jednak mieć jeszcze jedną funkcję, robiącą praktycznie to samo co x, ale z pewnymi dodatkami. Nie chcemy też jednocześnie przepisywać jeszcze raz tego samego kodu.
Możemy więc dekorator wyobrazić sobie jako coś, co wykorzystuje podstawową funkcję, ale dodaje do niej swoje działanie. Trochę jak placek tortilli owija mięso w środku. Dekorator przyjmuje funkcję podstawową jako argument i dodaje jakieś działanie zdefiniowane w jego ciele. Tak właśnie dekorujemy funkcje.
Stosowanie dekoratorów
Przechodząc do praktyki stwórzmy sobie od razu funkcję, która będzie coś robić – przykładowo dodawać dwie liczby. Następnie utwórzmy kolejną funkcję, która będzei dodawać do niej jakiś tekst.
Powyższy kod jest banalny, przeczytajmy go zaczynając od funkcji add_nums. Definiujemy wymienioną funkcję, zwracającą wynik dodawania dwóch jej parametrów. Następnie definiujemy funkcję decorator_add_nums (tak, żeby nie było wątpliwości), która przyjmuje jako argument funkcję add_nums. Druga z wymienionych funkcji drukuje na ekran napis „adding nums” po czym zwraca wywołanie func_add_nums. Na koniec wywołujemy decorator_add_nums, która wywołuje add_nums z argumentami 1 i 2, po czym jej wynik jest wyświetlany. Nic trudnego. Mamy jednak problem.
Problem polega na tym, że funkcja add_nums(1,2)
jest wywoływana, a jej wynik (w tym przypadku 3
) jest przekazywany do decorator_add_nums
. W efekcie decorator_add_nums
pracuje z wynikiem (3
), a nie z funkcją add_nums
.. Ten kod nie przestrzega zatem podstawowej zadsady działania dekoratorów w Pythonie. Jest tak ponieważ dekoratory to funkcje, które przyjmują inną funkcję jako argument i zwracają nową funkcję, która opakowuje oryginalną funkcję, modyfikując lub rozszerzając jej zachowanie.
Funkcja decorator_add_nums
natomiast niewiele zmienia w działaniu funkcji add_nums
, po prostu zwraca przekazaną funkcję bez żadnych modyfikacji. Nasz kod musi więc ulec pewnym zmianą. Aktualnie końcowa linijka po prostu sprawia, że funkcja add_nums(1,2) jest wywoływana i na jej wyniku pracuje funkcja decorator_add_nums. Druga z nich dopisuje więc „adding num” i tylko zwraca wynik pierwszej – co nie jest niczym wybitnym. Nie przekazaliśmy funkcji, a jej wynik.
Naprawa 'dekoratorów’ z powyższego przykładu, czyli prawdziwe dekorowanie
Po kolei spełnijmy teraz założenia dekoratora, po pierwsze dekorator przyjmuje inną funkcję jako argument. Ograniczmy się najpierw tylko do tego.
Powyższy kod nie będzie działał, ale jak widzimy funkcja decorator_add_nums przyjmuje funkcje add_nums jako argument. Następne założenie do spełnienia mówi o tym, że dekorator tworzy i zwraca nową funkcję (tzw. wrapper), który modyfikuje lub rozszerza zachowanie oryginalnej funkcji. Zatem skupmy się na razie tylko na decorator_add_nums.
Dla jasności dodałem komentarze. Dzięki temu możemy zobaczyć, że mamy funkcję w funkcji. Konkretnie jest to wewnętrzna funkcja, która przejmuje rolę funkcji oryginalnej (przy okazji przyjmując dowolną ilość argumentów i argumentów słów kluczowych). Następnie dodawana jest logika wyświetlająca komunikat „adding nums”, żeby w następnej linii wywołać funkcję przyjętą na początku jako argument i przypisać jej wynik do zmiennej result. Później wewnętrzna funkcja wrapper zwraca ten rezultat, a funkcja zewnętrzna zwraca sam wrapper (czyli właśnie ten rezultat, zwrócony przez wrappera).
Możemy w skrócie powiedzieć, że dekorator ten zawiera:
- funkcję wewnętrzną wrapper, która:
- Przyjmuje argumenty i przekazuje je dalej do oryginalnej funkcji (func_add_nums)
- Dodaje dodatkową logikę (tutaj print(„adding nums”)
- Zwraca wynik działanie oryginalnej funkcji
- Funkcja wrapper jest zwracana przez dekorator
Następnie musimy już tylko urzyć dekoratora, dodając nad oryginalną funkcją tzw. syntactic sugar w postaci znaku at po którym podajemy nazwę dekoratora, który ma być użyty do 'udekorowania’ konkretnej funkcji.
Taka konstrukcja, jest tożsama z linijką:
Co po prostu mówi, że od teraz add_nums wskazuje na opakowaną wersję funkcji, zwróconą przez dekorator (wrapper). Pozwala to Nam teraz po prostu wywołać add_nums z podając argumenty tak jakby nigdy nic.
W wyniku takiego wywołania Python faktycznie wywołuje funkcję wrapper (zwróconą przez dekorator), a nie oryginalną add_nums. Następnie funkcja wrapper wykonję dodatkową logikę (wyświetla napis). Po tej czynności funkcja wrapper wywołuje oryginalną funkcję add_nums(1,2) i zwraca jej wynik.
Jak alternatywnie można wywoływać dekoratory
Jeśli dobrze prześledzić powyższe przykłady, można dojść do wniosku, że nie potrzebujemy używać konstrukcji ze znakiem at. Możemy po prostu stworzyć zmienną, a do niej przypisać referencję do dekoratora z przekazaną referencją do oryginalnej funkcji. Będziemy mogli wtedy tą zmienną-referencję (czyli jak każda zmienna) użyć jako funkcji.
Innymi słowy następuje tutaj po prostu ręczne przypisanie dekoratora. Czyli w linii 12 funckja decorator_add_nums jest wywoływana, a jako jej argument jest przekazywana funkcja add_nums. Wewnątrz decorator_add_nums natomiast func_add_nums wskazuje na add_nums. Następnie wrapper jest zwracana i zastępuje add_nums , a sama zmienna res przechowuje funckę wrapper. Następnie wyświetlamy efekt wywołania res jako funkcji.
Składnia @
jest standardem w Pythonie, a alternatywne podejście (ręczne przypisanie dekoratora) może być użyte w specyficznych sytuacjach, np. dla większej przejrzystości w bardziej skomplikowanych przypadkach.
1 komentarz do “Myśląc o Pythonie: Dekoratory”