Dekoratory często sprawiają problemy początkującym. Narzekania słychać także ze strony doświadczonych programistów z rodziny c plusowych. To dlatego, że dekoratory wykorzystują bajery specyficzne dla języka Python.

Zanim zaczniemy na poważnie, warto jeszcze wyjaśnić do czego służą dekoratory. Jak sama nazwa wskazuje do dekorowania funkcji, czyli dodawania do już istniejącej funkcji  jakiś dodatkowych funkcjonalności. Wiem, na tą chwile brzmi to trochę zawile.

Jednak bez paniki, powoli małymi kroczkami opanujemy i tego potwora.

By bez bólu stworzyć dekorator należy poznać parę prawd o Pythonie

1. Funkcja tak naprawdę jest instancją obiekt który możemy zawołać, a zatem nic nie stoi na przeszkodzie aby funkcje przypisać do zmiennej.

def moja_funkcja(): #całkiem zwykła funkcja
    print('działa')

uchwyt_do_funkcji = moja_funkcja # od teraz uchwyt_do_funkcji wskazuje na moja_funkcja
uchwyt_do_funkcji() #teraz uruchamiamy moja_funkcja używając uchwytu

Wywołanie tego kodu spowoduje wyświetlanie napisu działa. Oznacza to ,że uruchamiając funkcję o nazwie uchwyt_do_funkcji uruchamiamy funkcje moja_funkcja. Jeżeli każdą funkcję możemy traktować dwojako raz jako wywołanie funkcji ,raz jako zmienną trzymającą uchwyt do niej jak w tym wszystkim się nie pomylić. Odpowiedz jest prosta. Nawiasy „()”. Jeśli masz zamiar wywołać (uruchomić) funkcję zrób tak. „moja_funkcja()”. Jeśli chcesz użyć jej jako zmiennej trzymającej uchwyt (np. w celu przypisania jej do innej zmiennej) używasz samej nazwy „moja_funkcja”

 

2. Możliwe jest zrobienie funkcji wewnątrz funkcji

def funkjca_zewnętrzna():
  print('funkjca_zewnętrzna') 
  def funkca_wewnętrzna():
    print('funkca_wewnętrzna')
  funkca_wewnętrzna()

funkjca_zewnętrzna()

Uruchom w edytorze

Zauważcie, że by wewnętrzna funkcja została uruchomiona koniecznie jest jej jawne wywołanie.

3. Jak już wiemy funkcję można potraktować jako instancje obiektu i zapisać referencję do niej w zmiennej. Pokazaliśmy to w poprzednich przykładach. Idąc dalej tym tropem nic nie stoi na przeszkodzie by taka zmienna trzymająca referencję do funkcji była przyjmowana jako parametr innej funkcji. Możemy też napisać funkcję zwracając taką referencję.

def funkcja_do_przekazywania():
  print('funkcja do przekazywania')

def biore_funkcje_i_zwracam(funkcja):
  return funkcja

funkcja = biore_funkcje_i_zwracam(funkcja_do_przekazywania)

funkcja()

Uruchom w edytorze

Uzbrojeni w tą wiedzę mamy już wszystko co potrzebne do napisania dekoratora.
Czym są jednak te dekoratory i do czego jest to nam potrzebne

Czym jest dekorator

Dekorator to tak naprawdę funkcja (a dokładniej callable object), która jako parametr przyjmuje funkcje (tą którą ma udekorować), oraz zwraca referencje do już udekorowanej funkcji.

Dekorator

Na początek potrzebna nam funkcja, którą potem udekorujemy.
Dobrym kandydatem będzie funkcja dzieląca przez siebie dwie liczby.

def dzielenie(a,b):
    return a / b

Nasz dekorator będzie sprawdzać, czy aby na pewno nie próbujemy dzielić przez zero. Musi zatem przyjąć naszą funkcje a potem sprawdzić czy zmienna b nie jest równa zero.

def dzielenie(a ,b):
  return a / b

def dekorator(funkcja):
  def wraper(a ,b):
    if b != 0:
      return funkcja(a ,b)
  return wraper
    
dzielenie = dekorator(dzielenie)
print(dzielenie(2, 2))
print(dzielenie(2, 0))

Uruchom w edytorze

Jak widać teraz można użyć funkcji dzielnie bez obawy, że gdy pod zmienna be przypiszemy 0 będzie no nas kosztować wyjątek.

A teraz mała niespodzianka. Jeśli uważasz, że wywoływanie dekoratorów w sposób jaki zrobiliśmy to powyżej jest mało wygodny i bez sensu to masz rację. Chodziło o pokazanie co tam się dzieje pod maską. Kluczem do pisania operatorów jest zrozumienie jak działają. Jak już wszytko jest jasne czas na trochę magii. Właściwą formą użycia operatora jest zastosowanie notacji @. Ponieważ w magię trzeba uwierzy szybko skaczemy do kolejnego przykładu.

def dekorator(funkcja):
  def wraper(a ,b):
    if b != 0:
      return funkcja(a ,b)
  return wraper

@dekorator
def dzielenie(a ,b):
    return a / b

print(dzielenie(4, 2))
print(dzielenie(2, 0))

Uruchom w edytorze

O wiele lepiej. Wszytko działa jak wcześniej. Za każdym razem jak użyjesz funkcji dziennie najpierw wołany jest nasz dekorator.

 

Dekoratory sparametryzowane

Możliwe jest przekazywanie parametrów do naszego dekoratora. Dla celów testowych przekażmy do naszego dekoratora argument tekstowy. Coś w stylu „dzielnie przez zero to zło”. Wyświetlimy go w przypadku próby dzielenia przez zero.

def param_dekor(komunikat):
  def dekorator(funkcja):
    def wraper(a ,b):
      if b != 0:
        return funkcja(a ,b)
      else:
        return komunikat
    return wraper
  return dekorator

@param_dekor('nie dziel przez zero!!!')
def dzielenie(a ,b):
    return a / b

print(dzielenie(4, 2))
print(dzielenie(2, 0))

Uruchom w edytorze

Wyszyto co należało zrobić to dodać kolejną warstwę zagłębienia. Dekoratory są bardzo potężną bronią w arsenale Pythona i umożliwią rozwiązania wielu problemów w bardzo ładny i schludny sposób. Na deser bardzo ciekaw użycie dekoratora.

Dekorator liczący wywołania funkcji

Świetny przykład użycia dekoratora do zliczania wywołań funkcji. Otwiera oczy na wiele ciekawych aspektów programowania w języku Python 🙂

def licz_wywolania(func):
    def wraper(x):
        wraper.licznik += 1
        return func(x)
    wraper.licznik = 0

    return wraper

@licz_wywolania
def funkcja(x):
    return x + 1

print(funkcja.licznik)
for i in range(10):
    funkcja(i)
    
print(funkcja.licznik)

Uruchom w edytorze

To tyle na dzisiaj. Miłej zabawy z dekorowaniem.