Jedną z ważnych koncepcji, na drodze do stania się sprawnym programistą Python, są dekoratory. Mieliśmy już z nimi do czynienia w przypadku nauki programowania obiektowego, gdzie wykorzystywaliśmy dekorator, aby zadeklarować metody abstrakcyjne. Jest to jednak wierzchołek góry lodowej, a same dekoratory, okazują się być niezwykle przydatne oraz często spotykane. Dzisiaj zobaczymy czym są i jak je stosować w praktyce. Zaczynajmy!

Dekoratory, między innymi, umożliwiają nam modyfikację działania funkcji, bez ingerencję w samą funkcję. Przykładowo. Powiedzmy, że chcemy:

  • aby, każde wywołanie funkcji, było logowane do pliku
  • zagwarantować, aby użytkownik musiał być zalogowany, aby wykonywać pewne funkcje
  • modyfikować wyniki pewnych funkcji
  • zagwarantować, że pewne metody w klasach, były abstrakcyjne (tak jak widzieliśmy w naszych przykładach)
  • mierzyć czas wykonywania funkcji

W regularny, standardowy sposób, potrzebowalibyśmy modyfikować każdą funkcję, której zachowanie chcemy rozszerzyć. Czasami jednak mamy potrzebę, czy to wprowadzenia pewnej zmiany, w wielu funkcjach, czy też rozszerzenie zachowania starej funkcji, napisanej przez kogoś innego, czy też upewnienie się, że kilka funkcji wykonuje takie same akcje (jak np sprawdzenie czy użytkownik jest zalogowany).

W tedy, z pomocą przychodzą nam dekoratory.

Użycie dekoratora jest bardzo proste, i każdy to potrafi. Polega na umieszczeniu przed definicją funkcji @<nazwa dekoratora>. Jeżeli, przykładowo, mamy dekorator, który sprawia, że, każde uruchomienie funkcji jest logowane, możemy dodać taki dekorator do wybranych funkcji, poprzez:

@loguj
def przykładowaFunkcja():
    print ("Moje uruchomienie zostanie zalogowane")

przykładowaFunkcja()

Co już nie jest takie oczywiste, i wielu ludzi tego unika, to pisanie własnych dekoratorów. My jednak zobaczymy za chwilę, że nie taki diabeł straszny, i nauczymy się czerpać z nich korzyści.

Niezbędna wiedza podstawowa, aby pisać dekoratory

Zanim napiszemy nasze pierwsze dekoratory, zróbmy podsumowanie potrzebnej, do tego wiedzy. Wtedy napisanie dekoratorów przyjdzie nam bardzo gładko.

Funkcja jako parametr

W Python, mamy możliwość przekazywania funkcji, jako parametr. Widzieliśmy tego zastosowanie, w ostatnim mini projekcie. W skrócie wygląda to tak:

def wykonaj(funkcja, a, b):
    
    x = funkcja (a, b)
    return x

def dodaj(a,b):
    return a+b

def odejmij(a,b):
    return a-b

wykonaj(dodaj, 2, 3)
wykonaj(odejmij, 2, 3)

Funkcja, wewnątrz funkcji

W Python, możemy tworzyć funkcje, wewnątrz funkcji. Zobaczmy:

def główna():
    
    def wewnętrzna(a,b):
        return a*b
    
    x = wewnętrzna(2,3)
    return x

główna()

Funkcja, może zwracać funkcje

Tak samo jak funkcja, możeme przyjmować funkcję, jako parametr, tak samo, może zwracać funkcję. Zobaczmy:

def funk():
    
    def funkWew(a,b):
        return a * b
    
    return funkWew

x = funk()
x(3,3)

Wywołaliśmy funkcję funk(), która zwróciła funkcję funkWew. Ta została przekazana do ‚x’, a następnie ją wywołaliśmy.

Jak działa dekorator

Dekorator w Python, to nic innego jak funkcja, która przyjmuje jako parametr funkcję, którą chcemy udekorować, następnie tworzy wewnętrzną funkcję, która nadpisuje działanie przekazanej przez parametr funkcji, a następnie zwraca wewnętrzną funkcję. 🙂

Tym samym, zwracana funkcja, jest funkcją przekazaną jako parametr, ‚wzbogaconą’ o dodatkowe działania – ‚udekorowaną’

I tym samym, proces pisania dekoratora, składa się z 3 wyżej wymienionych elementów, wiedzy podstawowej.

Zobaczmy taki dekorator w praktyce. Na początku w prostszy sposób, a potem w sposób, skrócony, który umożliwia nam Python.

Prostszy sposób:

def zwykłaFunkcja():
    print ("To jest zwykła funkcja")

def dekor(funkcja):
    
    def wew():
        print("Dekorujemy funkcję")
        return funkcja()
        
    return wew

nowaFunkcja = dekor(zwykłaFunkcja)
nowaFunkcja()

A, w wyniku otrzymamy:

Co się dzieje?

  1. Definiujemy niewinną funkcję, którą nazywamy ‚zwykłaFunkcja’, a która, tylko, drukuje napis.
  2. Wprowadzamy druga funkcje, którą nazwaliśmy ‚dekor’. Rolą tej funkcji, jest ‚udekowowanie’ przekazane mu w parametrze funkcje.
  3. Funkcja ‚Dekor’, tworzy funkcję wewnętrzną ‚wew()’, która z jednej strony wykonuje ‚dekorację’, czyli w naszym przypadku, dodaje swój napis, z drugiej strony, wywołuje funkcję, przekazaną w parametrze do ‚dekor’
  4. Na końcu, wywołujemy funkcję ‚dekor’, przekazujemy w parametrze funkcję ‚zwykłaFunkcja’, a w wyniku dostajemy nową funkcję, która jest udekorowaną funkcją ‚zwykłaFunkcja’
  5. W momencie jej wywołania, dostaniemy:

Wykorzystajmy jeszcze możliwości, jakie daje nam Python, w kwestii pisania dekoratorów i użyjemy, znany nam sposób @<nazwa dekoratora>

def dekor(funkcja):
    
    def wew():
        print("Dekorujemy funkcję")
        return funkcja()
        
    return wew

@dekor
def zwykłaFunkcja():
    print ("To jest zwykła funkcja")

nowaFunkcja()

Jest to znaczące ułatwienie, które oferuje Python. Jednocześnie nasz kod staje się czytelniejszy. Nasz dekorator ‚@dekor’, możemy umieścić przed każdą funkcją, którą chcemy udekorować.

Jest to bardzo dobry moment, aby przed przejściem do kolejnego punktu, skopiować powyższy kod, spróbować go zmodyfikować, a potem napisać swój prosty dekorator.

Dekorowanie funkcji z parametrami

W powyższym przykładzie, zobaczyliśmy proces pisania dekoratorów, który okazał się, nie taki straszny. Co prawda nasz dekorator, miał bardzo prostą funkcję do wykonania, jednak każdy inny będzie mieć taką samą logikę. Zanim przejdziemy do kolejnych przykładów oraz ćwiczeń, została nam jeszcze jedna, ważna kwestia do omówienia. Mianowicie dekorowanie funkcji, które przyjmują parametry.

Do tego celu, użyjemy sposobu na przyjmowanie przez funkcje dowolnej liczby parametrów, mianowicie *args, **kwargs. Cała logika, naszego dekoratora, zmieni się tylko i wyłącznie, w funkcji ‚wew’, która teraz przyjmuje jako parametr ‚*args, **kwargs’, oraz wywołuje ‚funkcje’ również z ‚*args, **kwargs’.

def dekor(funkcja):
    
    def wew(*args, **kwargs):
        print("Dekorujemy funkcję")
        return funkcja(*args, **kwargs)
        
    return wew

@dekor
def zwykłaFunkcja(a, b, c):
    print ("To jest zwykła funkcja, która dodaje liczby:")
    print (a+b+c)

zwykłaFunkcja(1,2,3)

A wywołanie naszego kodu, da nam następujące wyniki:

W ten sposób, możemy podejść do każdego dekoratora, który ma dekorować funkcję potencjalnie przyjmującą parametry.

Podsumowując. Dekoratory są bardzo użytecznym mechanizmem w Python, który podnosi nasz poziom zdolności programistycznych, w tym języku. Z jednej strony, może pojawić się konieczność jego napisania, z drugiej, jak zobaczymy w przypadku kursu odnoście popularnych modułów i pakietów w Python, wielokrotnie będziemy z nich korzystać. Zanim przejdziemy do dalszych lekcji kursu Python, dla NIE tylko początkujących, przyjrzyjmy się ćwiczeniom, które dalej zobrazują zastosowanie dekoratorów.

W następnej lekcji, podejmiemy się kolejnego mini projektu, w którym będziemy dalej wzbogacać naszego agenta, o kolejne narzędzia umożliwiające szybką i sprawna pracę w terenie. Zapraszamy!

Ćwiczenia

Ćwiczenie 1: Napisać swój pierwszy dekorator, którym należy udekorować dwie proste funkcje. Celem dekoratora, jest drukowanie informacji, która funkcja została wykonana.

ps.

Python to bardzo ciekawy język. Bezpośrednio w kodzie źródłowym, mamy dostęp do tak zwanych metadanych, Jedną z użytecznych rzeczy które możemy zrobić, to uzyskać dostęp do nazwy zmiennej wewnątrz dekoratora, przy użyciu funkcja.__name__.

Tym samym, jak zrobimy print(funkcja.__name__) to uzyskamy informację, która z udekorowanych funkcji została wykonana.

Ćwiczenie 2: Napisać dekorator, mający za zadanie, drukować informację o czasie wykonywania funkcji. Dekorator, powinien być gotowy dekorować funkcje przyjmujące różną ilość parametrów.

ps.

Czas wykonania pewnego zadania, możemy zmierzyć za pomocą modułu time i funkcji time()

import time
start = time.time()
time.sleep(1)
koniec = time.time()
print(koniec - start)

Wynik tego kodu, da nam informację, że jego wykonanie trwało około 1 sekundy

Rozwiązanie 1:

def dekor(funkcja):
    
    def wew():
        print(funkcja.__name__)
        return funkcja()
    return wew
    
@dekor
def pierwsza():
    print("zzzzzzzzzzzz")
    
@dekor
def druga():
    print("kkkkkkkkkrrrr")
    
pierwsza()
druga()
pierwsza()

Rozwiązanie 2:

import time

def dekor(funkcja):
    
    def wew(*args, **kwargs):
        start = time.time()
        x = funkcja(*args, **kwargs)
        koniec = time.time()
        print(funkcja.__name__, "wykonywała się", koniec - start, "sekund.")
        return x
    return wew

@dekor
def dodaj(a,b):
    time.sleep(1)
    return a+b

@dekor
def odejmij(a,b,c):
    time.sleep(0.5)
    return a-b-c

print(dodaj(2,2))
print(odejmij(1,2,3))

Python, NIE tylko dla początkujących

  1. Wstęp do kursu
  2. Moduły i pakiety
  3. Programowanie obiektowe – wstęp
  4. Programowanie obiektowe – dziedziczenie
  5. Programowanie obiektowe – hermetyzacja
  6. Mini projekt – Organizer
  7. Funkcje rekurencyjne w Python
  8. Pisanie skryptów w Python
  9. Mini projekt – tajny agent, generator haseł
  10. *args oraz **kwargs
  11. Dekoratory w Python <– bieżąca lekcja
  12. Mini projekt – tajny agent – kontakty
  13. Odwzorowanie list
  14. Podsumowanie oraz dalsze kroki