Cześć,
temat wzorców projektowych to temat rzeka:) przyglądając się mu można jednak łatwo zauważyć że dzielą się ona na te które przyjęły się szerzej jak i te które, no w sumie istnieją, ale nie słychać o nich za wiele.
Jednym z takich szumnych wzorców jest wzorzec dekorator.
Czym jest wzorzec dekorator od strony formalnej
Omawiany przez nas wzorzec należy do grupy wzorców strukturalnych, a więc takich które opisują struktury powiązanych ze sobą obiektów.
Jego działanie opiera się na opakowywaniu oryginalnej klasy, nowa klasą „dekorującą”. Czyli po prostu przekazuje się oryginalny obiekt jako parametr konstruktora dekoratora, metody dekoratora wywołują metody oryginalnego obiektu i dodatkowo implementują nową funkcję.
Aby zachować pełnie informacji formalnych – oto diagram klas tego wzorca:
Dekorator w praktyce – JAVA
Taki chyba najbardziej znanym przykładem użycie dekoratora w Javie jest jest klasa FileInputStream – posiada ona kilka dekoratorów i dodaje się do niej kolejne funkcjonalności w taki sposób:
FileInputStream fis = new FileInputStream("/objects.gz");
Tworzymy strumień, ale chcemy by był on buforowany, więc:
BufferedInputStream bis = new BufferedInputStream(fis);
Wszystko super, ale nasz plik jest skompresowany, więc dodajemy:
GzipInputStream gis = new GzipInputStream(bis);
I plik zawiera zserializowane obiekty Javowe, wiec dodajemy:
ObjectInputStream ois = new ObjectInputStream(gis);
No i finalnie możemy użyć naszego strumienia:
MyObj ungzipedDeserializedObj = (MyObj) ois.readObject();
Odczytując z niego jakiś obiekt.
Warto zauważyć że kolejne opakowywanie wcześniej stworzonego obiektu dodawało mu kolejne możliwości, odpowiednio – pierw zapewniało buforowanie odczytu, potem dekompresowało a na końcu jeszcze deserializowało. Pełna magia :)
Dekoratory w Pythonie
Dobra, ale dość przygód z Javą, przejdźmy do Python’a.
Dekoratory w Pythonie opierają się przede wszystkim na dwóch założeniach:
- funkcja może przyjąć jako argument inną funkcję
- wewnątrz funkcji można stworzyć kolejną funkcję
Ale zerknijmy na przykład:
1 2 | def say_something(): print("Hello!!") |
Ten potężny kawałek kodu nie robi nic innego jak printuje słowo „Helo!!”. A teraz ten kawałek kodu:
1 2 3 4 5 6 7 8 9 | from datetime import datetime def disable_at_night(func): def wrapper(): if 7 <= datetime.now().hour < 22: func() else: pass return wrapper |
Tutaj już jest ciekawiej – definiujemy funkcję o nazwie disable_at_night która przyjmie jako argument inną funkcje i zwróci jeszcze inną funkcję. Zauważ że w środku stworzonej przez nas funkcji jest funkcja wrapper i to właśnie ona jest wracana… Ale w niej jest wykonywana funkcja przekazana jako argument. Przeanalizuj to dokładnie, bo właśnie w tej chwili stworzyliśmy dekorator.
I nasz cały kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from datetime import datetime def say_something(): print("Hello!!") def disable_at_night(func): def wrapper(): if 7 <= datetime.now().hour < 22: func() else: pass return wrapper disable_at_night(say_something) |
Który spowoduje wykonanie się funkcji say_something tylko jeśli aktualna godzina jest pomiędzy 7mą a 22gą – czyli w dzień.
Czekaj, stop!
Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
Python pozwala na pewien mały „syntax sugar” i daje możliwość zamienienia wywołania:
disable_at_night(say_something)()
na
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from datetime import datetime def disable_at_night(func): def wrapper(): if 7 <= datetime.now().hour < 22: func() else: pass return wrapper @disable_at_night # <--- def say_something(): print("Hello!!") say_something() # <--- |
Muszę przyznać że składnia ala adnotacja jest bardzo wygodna!
Dekorator z argumentami
W Pythonie możemy stworzyć dekorator z argumentami. Weźmiemy teraz nasz przykład z góry i przepiszemy nasz dekorator by był bardziej generyczny – pozwolimy przekazać godziny w argumencie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from datetime import datetime def run_only_between(_from=7, _to=22): def real_decorator(func): def wrapper(): if _from <= datetime.now().hour < _to: func() else: pass return wrapper return real_decorator @run_only_between(7, 21) def say_something(): print("Hello!!") say_something() # <--- |
Poprzednio funkcja nazywała się disable_at_night – teraz stała się bardziej elastyczna, co spowodowało że dostała inną nazwę – run_only_between.
Zauważ że nasz dekorator wzbogacił się o jeszcze jedną funkcję w sobie! Mamy już funkcję w funkcji w funkcji :)
I teraz tak, wykonując funkcję say_something stos wywołań wygląda tak:
- run_only_between(7, 21) – zwróci funkcję real_decorator
- zwrócona funkcja real_decorator nie ma nadpisanych argumentów więc przyjmie w argumencie funkcję którą ma dekorować
- funkcja real_decorator zwróci funkcję wrapper która dodaje nową funkcjonalność, uruchamiając warunkowo dekorowaną funkcję
Przyznaj że sprytne:)
Nakładanie się dekoratorów
Zerknij na taki kawałek:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from datetime import datetime def log_before_and_after(func): def wrapper(): print("before") result = func() print("after") return result return wrapper def run_only_between(_from=7, _to=22): def real_decorator(func): def wrapper(): if _from <= datetime.now().hour < _to: func() else: pass return wrapper return real_decorator @log_before_and_after @run_only_between(7, 22) def say_something(): print("Hello!!") say_something() # <--- |
Dodaliśmy tu jeszcze jedna funkcję która jest użyta jako dekorator – dodaje logowanie przed i po wykonaniu funkcji. Oczywiście dekoratory są wykonywane od góry, więc nawet jeśli run_only_between wytnie nam wywołanie naszej funkcji, to „before” i „after” nadal się pojawi.
Zmierzając do brzegu
Mi osobiście dekoratory w Pythonie bardzo się podobają – są sprytne i takie, hmm, funkcjonalne? Tak, to chyba dobre słowo. Pisząc „funkcjonalne” mam na myśli że można naprawdę wiele rzeczy nimi załatwić nie niszcząc przy tym architektury.
Mateusz Mazurek
Jakies przyklady zastosowania dekoratorow?
Cześć, z dwa były w samym artykule:) możesz użyć np do mierzenia czasu wykonania się funkcji. Ogólnie wszedzie tam gdzie chcesz rozszerzyć funkcję o dodatkowe możliwości :)
Trzeba pamiętać, że Pythonowy dekorator nie ma nic wspólnego z wzorcem dekorator.
Z tym „nic” to bym się kłócił, bo Pythonowy nadal opakowuje i rozszerza działanie obiektu:P
Nie no pewnie ;) Ale tak chciałem podkreślić, że samo rozszerzanie obiektu co umożliwia dodanie tzw. dekoratora w Pythonie nie sprawia, że od razu jest spełniony wzorzec projektowy ;-))
Tak żeby osoby co przeczytają ten artykuł od razu nie myślały, że to jedno i to samo
Aaaaa no to tak, to zgadzam się :D
Dziękuję za pomoc w nauce o dekoratorach. Pozdrawiam
Cieszę się że pomogłem!
Cześć Mateusz,
Mi się dekoratory też podobają choć czasem trudno jest to sobie wyobrazić jak na przykład te wywołania funkcji ,w funkcji ,w funkcji.
Nie mam za dużo doświadczenia z dekoratorami ale pisałem w poście na moim blogu o dekoratorze @functools.lru_cache(). Jak chcesz to przeczytaj, jest po angielsku –
https://mstem.net/optimization-code-python-1/
Poza tym to nie wiedziałem że dekorator to wzorzec projektowy. Czy mógłbyś napisać o innych wzorcach projektowych w języku Python?
Cześć.