Mateusz Mazurek – programista z pasją

Czyli o użyciu Pythona i kilku innych technologii do tworzenia świetnej jakości aplikacji w oparciu o stabilny proces dostarczania oprogramowania.

Algorytmika Inżynieria oprogramowania Programowanie Programowanie webowe

Dekorator w Pythonie

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:

undefined

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:
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
Jeśli to Cię interesuje to zapraszam również na swoje social media.

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.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

9 komentarzy
Inline Feedbacks
View all comments
Daroo

Jakies przyklady zastosowania dekoratorow?

[…] działa! To, co jest tu ciekawe, to świadome użycie wiedzy na temat dziania dekoratorów i tego, że to zwykłe funkcje. Mam na myśli tutaj oczywiście dekoratory dataclass i […]

Paweł

Trzeba pamiętać, że Pythonowy dekorator nie ma nic wspólnego z wzorcem dekorator.

Paweł

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

Sławomir Kwiatkowski

Dziękuję za pomoc w nauce o dekoratorach. Pozdrawiam