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:

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:

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)
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.

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.

Dzięki za wizytę,
Mateusz Mazurek
Mateusz M.

Pokaż komentarze

    • 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

  • 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ść.

Ostatnie wpisy

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

2 tygodnie ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

3 miesiące ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

4 miesiące ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

4 miesiące ago

Praca zdalna – co z nią dalej?

Cześć, ostatnio w Internecie pojawiło się dużo artykułów, które nie były przychylne pracy zdalnej. Z drugiej strony większość komentarzy pod… Read More

4 miesiące ago

Podsumowanie: listopad 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W listopadzie dokończyłem cykl "Z mgły zrodzony" Sandersona. Tylko "Stop prawa" mi nie do… Read More

5 miesięcy ago