Blog JSystems - z miłości do programowania

Dekoratory w Pythonie



Dekoratory pozwalają dodać funkcjonalność do istniejącej funkcji. Funkcja dekorująca przejmuje rolę funkcji dekorowanej, wzbogacając ją o nową funkcjonalność.  Przykładem użycia może być np. mierzenie czasu wykonywania dekorowanych funkcji czy autoryzacja dostępu.


Aby rozpocząć definiowanie własnych dekoratorów musimy wiedzieć że:


- Funkcja może być przekazywana do innej funkcji jako parametr


- Funkcja może być zdefiniowana wewnątrz innej funkcji


- Funkcja może zwracać inną funkcję


Poniżej małe przypomnienie.


Funkcja jako argument


def obrob(fun,a,b):
    print ( fun(a,b) )

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

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

obrob(dodaj,10,5)
obrob(odejmij,10,5)


Wynik na konsoli:


15


5


Zadeklarowałem funkcję "obrob" która przez argumenty przyjmie jedną funkcję oraz dwie wartości liczbowe. Funkcja "obrob" zastosuje przekazaną przez peirwszy argument funkcję na pozostałych 2 argumentach i wyświetli wynik tej operacji. Następnie deklaruję dwie proste funkcje - jedna dodaje do siebie otrzymane argumenty, druga je od siebie odejmuje. Wywołuję funkcję "obrob" podając przez argumenty wartości liczbowe do obrobienia i funkcję która ma zostać na tych wartościach zastosowana.


Funkcja w funkcji


def zewnetrzna():
     def wewnetrzna(a,b):
         return a*b
     x=wewnetrzna(4,5)
     return x

print(zewnetrzna())


Wewnątrz funkcji "zewnetrzna" zadeklarowałem funkcję "wewnetrzna". Jest ona widoczna tylko z wnętrza funkcji "zewnetrzna" z linii znajdujących się pod deklaracją funkcji "wewnetrzna".


Zwracanie funkcji z funkcji


def zewnetrzna():
     def wewnetrzna(a,b):
         return a*b
     return wewnetrzna

x=zewnetrzna()
print( x(19,13) )



Funkcja "zewnętrzna" zwraca zaimplementowaną w swoim wnętrzu funkcję "wewnętrzna". Obiekt funkcji zostaje przypisany do zmiennej x - która od tej pory będzie reprezentowała przypisaną funkcję. Następnie wypisuję wynik działania odebranej funkcji na wartościach 19 i 13.


Tworzenie dekoratorów


Dekorator to funkcja która przyjmije przez argument dekorowaną funkcję, tworzy wewnętrzną funkcję która nadpisuje działanie funkcji dekorowanej a następnie zwraca tą funkcję wewnętrzną. Poniżej przykład:


def doopakowania():
     print('do opakowania')

def dekorator(fun):
     def opakowujaca():
         print('opakowująca')
         fun()
     return opakowujaca

dek=dekorator(doopakowania)
dek()


Wynik działania powyższego kodu na konsoli:


opakowująca


do opakowania


Zwróć uwagę że funkcja "dekorator" zwraca referencję do obiektu funkcji a nie jej wywołanie - stąd brak nawiasów przy "return opakowujaca". W powyższym przypadku funkcja "opakowujaca" dodaje dodatkowe zadanie wydruku informacji na konsoli. Ten sam efekt możemy osiągnąć również w ten sposób:


def dekorator(fun):
     def wewnetrzna():
         print('jakieś dodatkowe działania')
         fun()
     return wewnetrzna

@dekorator
def funkcja():
     print('jestem funkcją')

funkcja()


Użyłem tutaj zapisu "@dekorator". To co znajduje się po znaku "@" to nazwa funkcji dekorującej.


Zwróć uwagę że zmieniłem kolejność deklaracji funkcji. Dekorator musi zostać zdefiniowany przed jego użyciem.


Dekorowanie funkcji z parametrami


Dotychczas dekorowaliśmy funkcje nie przyjmujące żadnych argumentów. Co jednak jeśli takie się pojawią? Mamy dwie możliwości. Możemy posłużyć się zwykłym parametrem lub *args czy **kwargs. Najpierw pierwszy wariant:


def dekorowana(x):
     print(f'siema {x}')

def dekorator(fun):
     def wewn(x):
         print('przed')
         fun(x)
         print('po')
     return wewn

d=dekorator(dekorowana)
d('Andrzej')


Na konsoli zostało wyrzucone:


przed


siema Andrzej


po


Alternatywna technika dająca ten sam wynik działania:


def dekorator(fun):
     def wewn(x):
         print('przed')
         fun(x)
        print('po')
     return wewn

@dekorator
def dekorowana(x):
     print(f'siema {x}')

dekorowana('Andrzej')


Mamy więc dekorator zdolny do udekorowania jednoargumentowej funkcji. Chciałbym jednak by ten dekorator był uniwersalny i mógł być stosowany do funkcji mających różny typ i ilość argumentów. Może zechcemy np. monitorować czas wykonania poszczególnych funkcji? Funkcje mogą trafić się różne, mające różną ilość argumentów. Spróbujmy stworzyć dwie funkcje, jedną bezargumentową a drugą z jednym argumentem, a następnie ją udekorujmy naszym dotychczasowym dekoratorem:


def dekorator(fun):
     def wewnetrzna(x):
         print('dekorator!')
         fun(x)
return wewnetrzna

def dekorowana1():
     print('dekorowana 1')

def dekorowana2(argument):
     print(f'dekorowana 2. argument={argument}')

f1=dekorator(dekorowana1)
f2=dekorator(dekorowana2)
f1()
f2('test')


lub alternatywna metoda:


def dekorator(fun):
     def wewnetrzna(x):
         print('dekorator!')
         fun(x)
     return wewnetrzna

@dekorator
def dekorowana1():
     print('dekorowana 1')

@dekorator
def dekorowana2(argument):
     print(f'dekorowana 2. argument={argument}')

dekorowana1()
dekorowana2('test')


Nasza funkcja opakowująca wymaga podania argumentu x, stąd w obu wariantach zobaczymy na konsoli taki komunikat:


TypeError: wewnetrzna() missing 1 required positional argument: 'x'


Wynika to z próby wywołania funkcji "dekorowana1" która nie przyjmuje żadnego argumentu. Dekorator zadziała tylko dla funkcji przyjmującej jeden argument. Jeśli zakomentujesz linijkę "dekorowana1()", zobaczysz że dekorator zadziała dla funkcji "dekorowana2" ponieważ przyjmuje ona akurat jeden argument. Na konsoli zobaczysz:


dekorator!


dekorowana 2. argument=test


W jaki sposób stworzyć więc dekorator który będzie działał dla wszystkich funkcji, niezależnie od tego ile argumentów te funkcje przyjmują? Z pomocą przychodzą nam "*args" i "**kwargs":


def dekorator(fun):
     def wewnetrzna(*args,**kwargs):
         print('dekorator!')
         fun(*args,**kwargs)
     return wewnetrzna

@dekorator
def dekorowana1():
     print('dekorowana 1')

@dekorator
def dekorowana2(argument):
     print(f'dekorowana 2. argument={argument}')

dekorowana1()
dekorowana2('test')


Tym razem dekorator zadziała dla obu funkcji. Wynik na konsoli:


dekorator!


dekorowana 1


dekorator!


dekorowana 2. argument=test


Dekorowanie funkcji zwracających wartość


Dotychczasowe dekoratory obsługiwały tylko funkcje które nie zwracały żadnej wartości. Co jednak jeśli chcielibyśmy udekorować funkcję która coś zwraca?


def dekorator(fun):
     def wewnetrzna(*args,**kwargs):
         print('dekorator!')
         return fun()
     return wewnetrzna

@dekorator
def oddaj():
     return "wartość"

print(oddaj())


Przy wywołaniu funkcji dekorowanej w funkcji opakowującej wystaczy poprzedzić wywołanie funkcji klazulą "return". Wynik na konsoli:


dekorator!


wartość


Argumenty dekoratorów


Dekoratory mogą przyjmować argumenty. Przykład wywołania dekoratora z argumentem można spotkać na przykład tworząc aplikację webową we Flask:


@app.route('/widok')
def widok(request):
     pass


Przekazujemy do niego wartość "/widok" będący url który ma mapować dana funkcja. W jaki sposób samemu tworzyć takie dekoratory? Przyjmijmy że chciałbym stworzyć taki dekorator, któremu przez argument podam ile razy ma być powtórzone wykonanie funkcji dekorowanej. Wywołanie ma wyglądać w ten sposób:


@powtorz(10)
def funkcja():
     print('cześć, jestem funkcją!')


W tym przypadku chciałbym powtórzyć wywołanie tej funkcji 10 razy. Aby to osiągnąć będę musiał zrobić jeszcze jedno zagnieżdżenie funkcji.


def powtorz(x):
     def dekorator(fun):
         def wewnetrzna(*args,**kwargs):
             for i in range(x):
                 fun(*args,**kwargs)
         return wewnetrzna
     return dekorator

def funkcja():
     print('cześć, jestem funkcją!')

f=powtorz(10)(funkcja)
f()


Funkcja "powtorz" przyjmie przez argument ilosc powtorzeń. Zwraca ona standardowy dekorator jaki budowaliśmy do tej pory, z tą tylko różnicą że w funkcji opakopującej "wewnętrzna" mamy dodatkową pętlę która powtórzy wywołanie funkcji tyle razy ile podamy przez argument x funkcji "powtorz". Skupmy się teraz na samym wywołaniu. Mam na mysli linie:


f=powtorz(10)(funkcja)
f()


W pierwszej linii jako argument dla funkcji "powtorz" przekazuję wartość 10. Funkcja ta zwraca nam funkcję "dekorator" i podajemy do jej argumentu naszą funkcję dekorowaną. W wyniku dostajemy handler do naszej opakowywanej funkcji którą następnie wywołujemy. To samo możemy osiągnąć wywołaniem:


@powtorz(10)
def funkcja():
     print('cześć, jestem funkcją!')


funkcja()


Na konsoli dziesięciokrotnie pojawia się komunikat "cześć, jestem funkcją!".


 

Przyjdź do nas na szkolenie z języka Python! Mamy szereg szkoleń w ofercie, od podstawowych po aplikacje webowe z użyciem Django, analizę danych, tesowanie, machine learning i wiele innych.
Sprawdź dostępne szkolenia Python
Zapisz się do newslettera aby otrzymywać najnowsze świeżynki pojawiające się na blogu!