Pierwsza funkcja dekorująca

Nasz kod nie wygląda za dobrze. Ale z pomocą przychodzą nam dekoratory, które nie modyfikują oryginalnej funkcji/metody a jedynie rozszerzają jej działanie.

Brzmi to trochę skomplikowanie i od razu nasuwa się pytanie – jak to zrobić? Dekorator nie jest niczym innym jak zwykłą funkcją, która przyjmuje jako argument inną funkcję, którą ma opakować.

Funkcje można tak przekazywać?

Oczywiście! Funkcje mogą zachowywać się jak zwykłe obiekty. Tajemnica tkwi w podwójnych nawiasach, które stawiamy przy wywołaniu funkcji () 😉 W następnym akapicie omawiamy związane z tym przypadki.

Przykładowo, możemy zastosować funkcję tak:

def print_hello_jaktestowac():
    print('Hello jaktestowac!')

print_hello_jaktestowac

Tutaj nic się nie stanie ale jak zrobimy:

def print_hello_jaktestowac():
    print('Hello jaktestowac!')

print(print_hello_jaktestowac)

Dostaniemy coś takiego:

<function print_hello_jaktestowac at 0x038AA8E8>

Czyli informację o obiekcie. Nasza funkcja bez (), czyli print_hello_jaktestowac, zachowuje się jak zwykły obiekt. Możemy też na przykład przekazać ją do innej funkcji albo przypisać ją do jakiejś zmiennej i potem ją wywołać:

def print_hello_jaktestowac():
    print('Hello jaktestowac!')

hello = print_hello_jaktestowac
hello()

Natomiast gdy zastosujemy (), czyli print_hello_jaktestowac() to funkcja nasza zostanie wywołała a my dostaniemy wynik – wypisany napis.

TIP: Dla szybkiego przypomnienia co zwracają funkcje, które nie zawierają na końcu konstrukcji return?

print_hello_jaktestowac()

Wypisze nam na konsoli:

Hello jaktestowac!

Zauważmy, że sama funkcja print_hello_jaktestowac() nic nie zwraca (nie zawiera return):

x = print_hello_jaktestowac()
print(x)

Zwróci nam:

Hello jaktestowac!
None

Dlaczego dostajemy None? Gdyż w print_hello_jaktestowac nie ma wyrażenia return, a każda funkcja i metoda domyślnie zwraca wartość None 😉 Sprawdźmy – gdy dodamy do funkcji print_hello_jaktestowac zwracaną wartość przez return w taki sposób:

def print_hello_jaktestowac():
    print('Hello jaktestowac!')
    return 'secret'

Powtarzając ćwiczenia z tego TIP otrzymamy nasz zwracany string zamiast None. Mądrzejsi o tą wiedzę wracamy do poprzedniej wersji funkcji print_hello_jaktestowac.

Funkcja dekoratora

Stwórzmy w takim razie pustą funkcję o nazwie simple_current_time_wrapper. Zauważ, że już nazwa wskazuje, że będziemy opakowywać.

import datetime

def simple_current_time_wrapper():
    pass

def print_hello_jaktestowac():
    print('Hello jaktestowac!')


print(datetime.datetime.now())
print_hello_jaktestowac()

# more code...

print(datetime.datetime.now())
print_hello_jaktestowac()

Czarów tutaj nie ma – lecimy dalej. Teraz w funkcji simple_current_time_wrapper napiszemy wewnętrzną funkcję. Tu możesz zapytać:

A cóż to za abominacja?! Funkcja w funkcji?

Funkcja wewnętrzna

Python umożliwia tego typu konstrukcje, aczkolwiek ich przydatność jest ograniczona, gdyż z takich wewnętrznych funkcji można korzystać jedynie w obrębie zewnętrznej funkcji, w której zostały stworzone.

Poniższy fragment skryptu dobrze obrazuje tę sytuację:

def visit_town_innsmouth():

   def enter_innsmouth_tavern():
       print('You have entered the tavern.')

   def enter_innsmouth_hotel():
       print('You have entered the hotel. Beware!')

   print('You have arrived to Innsmouth...')
   # użycie dozwolone wewnętrznej funkcji
   enter_innsmouth_tavern()
   enter_innsmouth_hotel()

# użycie niedozwolone wewnętrznej funkcji - poza obrębem funkcji, w której została utworzona:
enter_innsmouth_tavern()

Uruchomienie powyższego skryptu zwróci nam błąd dotyczący braku widoczności funkcji, którą chcemy wywołać:

NameError: name 'enter_innsmouth_tavern' is not defined

Nic dziwnego, w ostatniej linii skryptu następuje użycie wewnętrznej funkcji w niedozwolonym miejscu.

Funkcja wewnętrzna dekoratora

Wracamy do pisania kodu w simple_current_time_wrapper. Nowa funkcja będzie miała nazwę wrapper. Zadeklarujemy ją bez parametrów. Będzie to funkcja wewnętrzna, którą na razie zaślepimy za pomocą pass:

import datetime


def simple_current_time_wrapper():
    def wrapper():
       pass


def print_hello_jaktestowac():
    print('Hello jaktestowac!')


print(datetime.datetime.now())
print_hello_jaktestowac()

# more code...

print(datetime.datetime.now())
print_hello_jaktestowac()

Dodatkowo na końcu simple_current_time_wrapper zwróćmy nowo utworzoną funkcję wrapper. Robimy to za pomocą wyrażenia return i jej nazwy. Czyli zwracamy funkcję jako obiekt a nie rezultat jej wykonania, oznacza to, że zwrócimy ją bez (). Całość przyjmie postać:

import datetime

def simple_current_time_wrapper():
    def wrapper():
       pass

    return wrapper

def print_hello_jaktestowac():
    print('Hello jaktestowac!')


print(datetime.datetime.now())
print_hello_jaktestowac()

# more code...

print(datetime.datetime.now())
print_hello_jaktestowac()

Co to oznacza, że zwracamy tę funkcję jako obiekt?

Testujemy wyprodukowaną funkcję

W osobnym pliku scratches skopiuj kod samej funkcji simple_current_time_wrapper:

def simple_current_time_wrapper():
    def wrapper():
       pass

    return wrapper

Następnie dodaj zamiast pass np: print("Hello").

def simple_current_time_wrapper():
    def wrapper():
       print("Hello")

    return wrapper

I teraz wywołaj funkcję simple_current_time_wrapper:

def simple_current_time_wrapper():
    def wrapper():
       print("Hello")

    return wrapper

simple_current_time_wrapper()

Po uruchomieniu tego kodu nic się nie stanie. To dlatego, że tak naprawdę zwracamy tylko obiekt funkcji wrapper. Trzeba ten obiekt dopiero przypisać do zmiennej aby coś z nim więcej zrobić:

def simple_current_time_wrapper():
    def wrapper():
       print("Hello")

    return wrapper

func_as_object = simple_current_time_wrapper()

I mamy teraz w zmiennej func_as_object zwróconą funkcję wrapper. To powinno być jasne. Skoro func_as_object to jest funkcja wrapper to możemy ją wywołać. No i tym samym uzyskać wynik z funkcji wrapper – dodajmy więc wywołanie funkcji!.

def simple_current_time_wrapper():
    def wrapper():
       print("Hello")

    return wrapper

func_as_object = simple_current_time_wrapper()
func_as_object()

Wywołanie funkcji w ostatniej linijce w końcu przynosi oczekiwany rezultat. Jest to niezbyt proste zagadnienie ale ćwicząc tego typu konstrukcje możesz już powoli poczuć o co chodzi z obiektem funkcji a samym jej wywołaniem.

Oczywiście nie tworzymy tych konstrukcji po to aby sobie poczarować w kodzie – ta wiedza jest niezbędna przy tworzeniu dekoratorów. Wracamy zatem do naszego poprzedniego kodu i kontynuujemy budowanie pierwszego dekoratora.

Kod funkcji wewnętrznej dekoratora

Przypomnijmy, mamy funkcję simple_current_time_wrapper, która posiada funkcję wewnętrzną. Wywołanie simple_current_time_wrapper zwraca funkcję wewnętrzną.

Już jesteśmy blisko. Teraz zastąpimy pass kodem, który wypisze nam aktualną datę.

print(datetime.datetime.now())

Funkcja dekoratora przyjmie następującą postać:

def simple_current_time_wrapper():
    def wrapper():
       print(datetime.datetime.now())

    return wrapper

Czyli w końcu pojawił się jakiś konkret w wewnętrznej funkcji.

Mini Teraz Ty – Test funkcji dekoratora

Możemy już przetestować wywołanie naszej funkcji.

  1. Skopiuj jej kod i wklej go do nowego pliku.
  2. Powtórz sprytny zabieg z przypisaniem zwracanej wartości do zmiennej.
  3. Potem wywołaj tą zmienną jako funkcję.
  4. Nie zapomnij o imporcie datetime.

Twoje zadanie to tak naprawdę dodanie kodu niezbędnego do tego, aby uruchomić wewnętrzną funkcję co umożliwi wypisanie daty na konsolę.

Mini rozwiązanie – Test funkcji dekoratora

Kod mógłby wyglądać tak:


import datetime

def simple_current_time_wrapper():
   def wrapper():
       print(datetime.datetime.now())

   return wrapper

my_wrapper = simple_current_time_wrapper()
my_wrapper()

Wynik na konsoli to oczywiście data 😀
Po udanym eksperymencie wróć do naszego głównego kodu.

Parametr funkcji dekoratora

Dodamy do naszej nowej funkcji jeden parametr o nazwie function. Tak, funkcja simple_current_time_wrapper jako parametr przyjmie obiekt funkcji którą chcemy opakować:

import datetime

def simple_current_time_wrapper(function):
    def wrapper():
       print(datetime.datetime.now())

    return wrapper

def print_hello_jaktestowac():
    print('Hello jaktestowac!')


print(datetime.datetime.now())
print_hello_jaktestowac()

# more code...

print(datetime.datetime.now())
print_hello_jaktestowac()

Czyli powtórzmy, parametrem jest obiekt funkcji – czyli taka nie wywołana jeszcze funkcja co umożliwia nam uruchomienie jej w dowolnym momencie w kodzie.

Na koniec, po print(), w wewnętrznej funkcji dodamy wykonanie funkcji przekazanej w parametrze.

def simple_current_time_wrapper(function):
    def wrapper():
       print(datetime.datetime.now())
       function()

    return wrapper

Test pełnej funkcji dekoratora

I znowu zróbmy szybki eksperyment w nowym pliku scratches. Skopiujmy nasz obecny wygląd funkcji:

import datetime

def simple_current_time_wrapper(function):
    def wrapper():
       print(datetime.datetime.now())
       function()

    return wrapper

Dodajmy pod importem nową super prostą funkcję cat_sound():

import datetime

def cat_sound():
   print("miauuuu")

def simple_current_time_wrapper(function):
   def wrapper():
       print(datetime.datetime.now())
       function()

   return wrapper

Wywołajmy simple_current_time_wrapper i przypiszmy wynik tej funkcji do zmiennej.

cat_with_date = simple_current_time_wrapper()

To jeszcze za mało, wywołanie simple_current_time_wrapper musi się odbyć z wymaganym parametrem. Jako parametr podaj nazwę nowej funkcji cat_sound.

cat_with_date = simple_current_time_wrapper(cat_sound)

Czyli przekazaliśmy funkcję cat_sound jako obiekt. I w wyniku wykonania simple_current_time_wrapper(cat_sound) dostaliśmy obiekt funkcji wrapper. Ten obiekt (kryjący się pod zmienną cat_with_date) zawiera w sobie całą zawartość naszej funkcji wewnętrznej wrapper: czyli print i wywołanie przekazanej w parametrze funkcji.

Czas uruchomić cat_with_date! Dodajemy linię z wywołaniem tego obiektu – i nasz cały kod będzie wyglądał tak:


import datetime

def cat_sound():
   print("miauuuu")

def simple_current_time_wrapper(function):
   def wrapper():
       print(datetime.datetime.now())
       function()

   return wrapper

cat_with_date = simple_current_time_wrapper(cat_sound)
cat_with_date()

Jaki jest wynik – nic zaskakującego. Wykonane zostało wszystko co siedzi w wrapper czyli i czas i uruchomienie przekazanej funkcji w parametrze.


2019-01-17 08:51:28.742418
miauuuu

Finalny kod dekoratora

Wracamy do naszego bazowego kodu:

import datetime

def simple_current_time_wrapper(function):
    def wrapper():
       print(datetime.datetime.now())
       function()

    return wrapper

def print_hello_jaktestowac():
    print('Hello jaktestowac!')


print(datetime.datetime.now())
print_hello_jaktestowac()

# more code...

print(datetime.datetime.now())
print_hello_jaktestowac()

Tym samym uzyskaliśmy najprostszą funkcją dekorującą. Zwraca ona wewnętrzną funkcję, która wykonuje nasz dodatkowy kod a następnie wykonuje przekazaną w parametrze funkcję. Teraz pozostaje ją jakoś wykorzystać…

Zrobimy to dokładnie jak to było w przykładzie z miauczeniem kota 😀

2 komentarze

  1. co do wywołań obiektów funkcji to nie prościej zrobić to tak? tylko to juz nie jest wywołanie obiektu tylko samej funkcji?

    
    def simple_current_time_wrapper():
        def wrapper():
            print("Hello")
    
        return wrapper()
    simple_current_time_wrapper()
    

    i to tez zwroci nam Hello

    Avatar Dawid Kowalczyk
    1. Hej,
      Przesłany kod jak najbardziej będzie działał w zaprezentowanej formie 😉 Ale w tym przypadku 'Hello' wyświetlane jest w momencie wywołania simple_current_time_wrapper(). Rzuć okiem na poniższy kod, w którym pomiędzy deklaracją funkcji a jej wywołaniem chcemy wykonać jakąś czynność (w tym przypadku – wypisać Before my hello_wrapper...).

      def simple_current_time_wrapper():
          def wrapper():
              print('Hello')
      
          return wrapper()
      hello_wrapper = simple_current_time_wrapper()
      
      print('Before my hello_wrapper...')
      hello_wrapper()
      

      Po uruchomieniu powyższego skryptu otrzymamy następujący wynik:

      C:\Projects\tmp\venv\Scripts\python.exe C:\Projects\tmp\tmp.py
      Hello
      Before my hello_wrapper...
      Traceback (most recent call last):
        File "C:\Projects\tmp\tmp.py", line 10, in module
          hello_wrapper()
      TypeError: 'NoneType' object is not callable
      
      Process finished with exit code 1
      
      

      Wynik ten związany jest z faktem, że zawartość wrapper (czyli polecenie do wypisywania 'Hello') wykonywana jest w momencie return wrapper() (czyli wtedy gdy postawimy ()), czyli return tak na prawdę zwraca None. Dlaczego None? Gdyż def wrapper(): nie zawiera w sobie wyrażenia return, a jeśli return nie jest zawarty to zwracana jest właśnie wartość None.

      Aby to naprawić, musimy opóźnić wywołanie funkcji def wrapper, która wywoływana jest w return wrapper(). Pierwszym sposobem jest zmiana return wrapper() na return wrapper. Dziki temu wywołanie funkcji def wrapper nastąpi w linii hello_wrapper().

      Również, jeśli zamienimy hello_wrapper = simple_current_time_wrapper() na hello_wrapper = simple_current_time_wrapper to kod też zadziała poprawnie 🙂
      Mam nadzieję, że udało mi się odrobinę rozjaśnić tę kwestię 🙂

      Pozdrawiam,

      Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *