Opublikowano: 17-05-2017



Dekoratory zostały wprowadzone do języka w 2003 roku. Od tego czasu, w mojej ocenie, zdobyły szeroką popularność.

Z uwag czysto redakcyjnych, wszystkie przykłady zawarte w tym poście pisane są z myślą o Pythonie w wersji 3.6.

Dekoratory opisano w dwóch dokumentach — PEP 318 oraz PEP 3129. Pierwszy z nich został opublikowany w 2003 roku (Python 2.4) i dotyczy dekoratorów stosowanych do funkcji oraz metod. Po 4 latach (rok 2007, Python 3.0) opublikowano drugi dokument, w którym rozszerzono ich możliwości o dekorowanie klas.

Dekorator to obiekt, który można wywołać jak funkcję (klasa lub funkcja). Obiekt ten jest wrapperem dla pierwotnego obiektu.

Zobaczmy to na przykładzie. Utwórzmy najprostszy dekorator — funkcję, która będzie zwracała przekazany jej obiekt:

Opatrzmy teraz przykładową funkcję naszym nowo utworzonym dekoratorem:

Zapis ze znakiem @ przed funkcją to syntactic sugar i jest on równoważny następującemu zapisowi:

funkcja = dekorator(funkcja)

Operacje dokonywane są tutaj na nazwach. Nazwa funkcja przestaje wskazywać na obiekt reprezentujący naszą przykładową funkcję i od tego momentu wskazuje na obiekt zwrócony przez dekorator. W powyższym przypadku jest to ten sam obiekt, ale nietrudno jest sobie wyobrazić funkcję dekoratora w zmienionej postaci — zwracającej inny obiekt:

Wynikiem działania powyższego skryptu jest:

inna funkcja

Zgodnie z przedstawionym wcześniej równoważnym zapisem, nazwa funkcja wskazuje teraz na obiekt zwrócony przez dekorator — czyli inna_funkcja.

Powszechną praktyką jest umieszczanie inna_funkcja w funkcji dekoratora i wywołanie w niej pierwotnego obiektu (w omawianym przypadku jest to funkcja):

Jednym, z klasycznych już chyba, przykładów na dekoratory jest cache. Utworzymy dekorator o nazwie cache dla funkcji get_web_page zwracającej dane z serwisu internetowego:

Odwołanie do zawartości zdalnej może chwilę potrwać, dlatego zamiast rzeczywistego połączenia stworzyłem klasę WebMock. W przyszłości obiekt może zostać zmieniony, by faktycznie odwoływał się do treści umieszczonej w Internecie.

Na wydruku widzimy również funkcję cache będącą dekoratorem. Zwraca ona funkcję wrapper, która sprawdza, czy zna już podany adres i jeśli tak to zwraca wartość z cache, w przeciwnym wypadku wywołuje funkcję get_web_page odpowiedzialną za pobranie danych.

Pozostała część kodu powinna być dość oczywista. Jeśli nie, to zapraszam do dyskusji w komentarzach.

Przekazywanie argumentów

Do dekoratora możemy przekazać dowolne argumenty. W tym celu wykorzystamy nową funkcję, zobaczmy fragment kodu:

Trzeba przyznać, że ten kod niewiele różni się od poprzedniego. Funkcja cache, widoczna w linii 6, jest prawie taka sama. Zmiana widoczna jest w linii 9, wykorzystywany jest parametr funkcji cache_with_value.

Istotną zmianą jest dodanie wspomnianej funkcji cache_with_value. Przyjmuje ona parametr i zwraca funkcję cache. Spójrzmy na powiązaną z tym zmianę w linii 15, to jest wywołanie funkcji. W poprzednim przykładzie (linia 13) tego wywołania nie było. Ostatecznie w to miejsce zostanie wstawiona funkcja cache.

Spróbujmy zapisać to podobnie jak poprzednio, bez nadmiernej ilości cukru składniowego:

get_web_page = cache_with_value("It works!")(get_web_page)

W efekcie jest to równoważne:

get_web_page = cache(get_web_page)

Dodanie jednej funkcji pozwala zwiększyć możliwości dekoratorów. Metoda ta jest szeroko wykorzystywana i warto ją znać.

Dekorator w formie klasy

Do tej pory skupialiśmy się na dekoratorze jako funkcji, ale może on być też klasą. Zobaczmy zmodyfikowany pierwszy przykład:

Zapiszmy fragment odpowiedzialny za dekorator bez cukru składniowego:

get_web_page = cache(get_web_page)

Widzimy, że jest to wywołanie funkcji __init__, czyli nazwa get_web_page będzie wskazywała na instancję klasy. Podczas próby wywołania instancji klasy jak funkcji, wywołana zostanie metoda __call__.

Czy dekorator w formie klasy może przyjmować argumenty? Oczywiście, zobaczmy zmodyfikowany drugi przykład:

Widzimy, że został wykonany zabieg podobny do opisywanego już wcześniej. Najpierw tworzymy instancję klasy, po czym używamy jej jako dekoratora. Za pomocą funkcji __init__ możemy przekazać argumenty, natomiast wywołanie funkcji __call__ spowoduje udekorowanie funkcji.

Zapiszmy to bez cukru składniowego:

get_web_page = cache_with_value("It works!")(get_web_page)

Ciąg cache_with_value("It works!") to oczywiście wywołanie konstruktora obiektu, następnie na tym obiekcie wywoływana jest funkcja __calll__, do której przekazywany jest obiekt get_web_page. Widoczna tutaj sytuacja jest analogiczna, do omawianego wcześniej przekazywania parametrów za pomocą funkcji.

Dekorowanie klasy

Dekorowanie klasy odbywa się w sposób analogiczny, do dotychczas omówionych. Jedyną różnicą jest fakt, iż nie dekorujemy funkcji, a klasę.

Zobaczmy przykład zaproponowany przez theheadofabroom na stackoverflow:

Dlaczego to działa? Otóż rozpisując przykład, w kod pozbawiony cukru składniowego, otrzymujemy:

MyClass = singleton(MyClass)

MyClass wskazuje teraz na funkcję getinstance, którą składniowo wywołujemy w ten sam sposób, w jaki tworzymy nowy obiekt klasy:

my_class_instance = MyClass()

Funkcja ta sprawdza, czy dany obiekt już istnieje i go zwraca, w przeciwnym wypadku jest on tworzony.

Zauważmy, że pierwotna nazwa MyClass nie wskazuje na obiekt klasy, ale na specjalny typ reprezentujący klasę.

Dekorator wraps

Dla przejrzystości kodu, w poprzednich przykładach pominięto, istotny podczas tworzenia własnego dekoratora, dekorator wraps. Jego pominięcie powoduje utratę metadanych dekorowanej funkcji (np. docstringa). Zalecane jest, by był on dodawany do tworzonych dekoratorów.

Oto przykłady, bazujące na tych z dokumentacji, pokazujące utratę metadanych.

Wersja z dekoratorem wraps:

Wynik działania:

Calling decorated function
Called example function
example
Docstring

Wersja bez dekoratora wraps:

Wynik działania:

Calling decorated function
Called example function
wrapper
None


Comments powered by Disqus