Dekoratory
Jest to swobodne tłumaczenie artykułu autorstwa Kenta S. Johnsona.
Treść
Funkcje jako obiekty pierwszego rzędu
Funkcje w Pythonie są obiektami pierwszego rzędu [1]. Co oznacza, że mogą być przekazywane jako parametry wywołania innych funkcji oraz mogą być też wartościami zwracanymi przez te funkcje. Na przykład poniższa funkcja pobiera jako argument inną funkcję i wyświetla nazwę podanej funkcji:
>>> def nazwa_funkcji(f): ... print f.__name__i prosty przykład użycia:
>>> def foo(): ... print 'jakis komunikat' >>> nazwa_funkcji(foo) fooA oto przykład funkcji, która tworzy nową funkcję i zwraca ją jako wynik. W tym wypadku utworz_dodawanie tworzy funkcję, która dodaje stałą do jej argumentu:
>>> def utworz_dodawanie(x): ... def dodaj(y): ... return x + y ... return dodaj ... >>> dodaj5 = utworz_dodawanie(5) >>> dodaj5(10) 15
Opakowywanie funkcji
Łącząc obie powyższe możliwości możemy zdefiniować funkcję, która będzie pobierała inną funkcję w parametrze i zwracała jakąś funkcję utworzoną w sposób zależny od podanego parametru. Możemy na przykład utworzyć funkcję opakowującą [2] przekazaną funkcję, która będzie pokazywała informacje o każdym wywołaniu tej funkcji:
>>> def pokaz_wywolanie(f): ... def opakowanie(*args, **kwds): ... print 'Wywoluje:', f.__name__ ... return f(*args, **kwds) ... return opakowanie >>> bar = pokaz_wywolanie(foo) >>> bar() Wywoluje: foo jakis komunikatJeśli przypiszemy rezultat wywołania funkcji pokaz_wywolanie do tej samej nazwy co jej argument, to tym samym zastąpimy oryginalną wersję funkcji naszym opakowaniem:
>>> foo = pokaz_wywolanie(foo) >>> foo() Wywoluje: foo jakis komunikat
Dekoratory to tylko lukier składniowy
Jeśli śledziłeś ten tekst uważnie, to już rozumiesz podstawę działania dekoratorów, ponieważ dekorator jest tylko lukrem składniowym [3] dla koncepcji, której przed chwilą użyliśmy, a więc ten kod:
@pokaz_wywolanie def foo(): passjest odpowiednikiem tego:
def foo(): pass foo = pokaz_wywolanie(foo)
Każda funkcja, która przyjmuje inną funkcję jako jej jedyny argument i zwraca też funkcję lub inny obiekt wywoływalny [4], może być użyta jako dekorator.
Więc co nam to daje?
W pewnym sensie dekoratory nie wnoszą nic nowego; nie dodają żadnej nowej funkcjonalności do Pythona. To jest tylko nowa składnia dla starej idei. Jednak dekoratory pokazują, iż sama składnia także ma duże znaczenie - zyskały one dużą popularność i są szeroko stosowane w nowoczesnym kodzie pythonowym. Dekoratory są bardzo użyteczne przy refaktoryzacji kodu. Często zdarza się, iż ta sama funkcjonalność musi zostać wykonana w wielu funkcjach, np. odpisy do logów, czy synchronizacja wątków. Poza tym składnia dekoratorów pozwala na umieszczanie wyraźnej informacji (jeszcze przed definicją funkcji) w jaki sposób funkcja zostanie udekorowana, a więc w jaki sposób jej funkcjonalność zostanie zmieniona. To zdecydowanie zwiększa czytelność kodu.
Prawidłowa implementacja dekoratorów
Używając prostego dekoratora takiego jak powyżej, możemy zaobserwować pewne różnice pomiędzy oryginalną a udekorowaną funkcją:
>>> def bar(): ... ''' Funkcja `bar` ''' ... pass >>> bar.__name__, bar.__doc__, bar.__module__ ('bar', ' Funkcja `bar` ', '__main__') >>> import inspect >>> inspect.getargspec(bar) ([], None, None, None) >>> bar2 = pokaz_wywolanie(bar) >>> bar2.__name__, bar2.__doc__, bar2.__module__ ('opakowanie', None, '__main__') >>> inspect.getargspec(bar2) ([], 'args', 'kwds', None)Atrybuty funkcji (tzn. obiektu funkcji, bo jak pamiętamy funkcje także są obiektami) nie są kopiowane do naszej funkcji opakowującej, a poza tym nie zgadza się też sygnatura nowej funkcji w porównaniu do oryginalnej.
Atrybuty oryginalnego obiektu funkcji mogą być zachowane poprzez skopiowanie ich z funkcji oryginalnej. Oto lepsza wersja pokaz_wywolanie:
>>> def pokaz_wywolanie(f): ... def opakowanie(*args, **kwds): ... print 'Wywoluje:', f.__name__ ... return f(*args, **kwds) ... opakowanie.__name__ = f.__name__ ... opakowanie.__doc__ = f.__doc__ ... opakowanie.__module__ = f.__module__ ... opakowanie.__dict__.update(f.__dict__) ... return opakowanieW tej wersji atrybuty są już OK, ale sygnatura nadal się nie zgadza:
>>> bar2 = pokaz_wywolanie(bar) >>> bar2.__name__, bar2.__doc__, bar2.__module__ ('bar', ' Funkcja `bar` ', '__main__')Moduł functools (nowy w Pythonie 2.5) dostarcza dekoratora dla dekoratorów, który nazywa się wraps(). Przy jego pomocy powyższy przykład można zapisać tak:
>>> from functools import wraps >>> def pokaz_wywolanie(f): ... @wraps(f) ... def opakowanie(*args, **kwds): ... print 'Wywoluje:', f.__name__ ... return f(*args, **kwds) ... return opakowanie >>> bar2 = pokaz_wywolanie(bar) >>> bar2.__name__, bar2.__doc__, bar2.__module__ ('bar', ' Funkcja `bar` ', '__main__')Moduł decorator autorstwa Michele Simionato zawiera dekorator, który używa funkcji eval do tworzenia dekoratorów, które zachowują także sygnaturę dekorowanej funkcji.
Dekoratory z argumentami
Zapewne zauważyłeś pewną nowość w powyższym przykładzie: dekorator przyjmuje argument. Jak to działa?
Załóżmy, że mamy taki kod:
@wraps(f) def nic_nie_rob(*args, **kwds): return f(*args, **kwds)Zgodnie z definicją składni dekoratorów, to dokładnie odpowiada temu:
def nic_nie_rob(*args, **kwds): return f(*args, **kwds) nic_nie_rob = wraps(f)(nic_nie_rob)Aby to miało jakiś sens, to wraps musi być funkcją fabryczną tworzącą dekorator - funkcją, której zwracana wartość będzie dopiero właściwym dekoratorem. Innymi słowy wraps jest funkcją zwracającą funkcję, która pobiera jako argument funkcję i zwraca funkcję!
O rany!
Może jakiś prosty przykład? Możemy utworzyć funkcję, która wielokrotnie wywoływałaby funkcję, którą dekoruje? Dla ustalonej liczby takich wywołań jest to bardzo proste:
>>> def foo(): ... print 'jakis komunikat' >>> def powtorz3(f): ... def opakowanie(*args, **kwds): ... f(*args, **kwds) ... f(*args, **kwds) ... return f(*args, **kwds) ... return opakowanie >>> f3 = powtorz3(foo) >>> f3() jakis komunikat jakis komunikat jakis komunikatAle załóżmy, że chcielibyśmy móc podać liczbę wywołań w parametrze. Wówczas potrzebujemy funkcji, która zwróci dekorator. Ten zwrócony dekorator będzie podobny do powtorz3 powyżej. Wymaga to dodania jednego poziomu zagnieżdżenia funkcji:
>>> def powtorz(n): ... def powtorz_nrazy(f): ... def opakowanie(*args, **kwds): ... for i in range(n): ... ret = f(*args, **kwds) ... return ret ... return opakowanie ... return powtorz_nrazyTutaj powtorz jest funkcją fabryczną tworzącą dekorator a powtorz_nrazy jest faktycznym dekoratorem. Funkcja opakowanie jest naszą funkcją opakowującą, która będzie wywoływana w zastępstwie funkcji oryginalnej. A oto przykład użycia ze składnią dla dekoratorów:
>>> @powtorz(4) ... def bar(): ... print 'Funkcja bar' >>> bar() Funkcja bar Funkcja bar Funkcja bar Funkcja bar
Przykłady z życia wzięte
Trudno jest wyszukać w rzeczywistym kodzie przykłady dekoratorów dla początkujących. Dekoratory mają tendencję do bycia albo bardzo prostymi, co nie wnosi zbyt wiele do powyższych przykładów, albo znacznie bardziej skomplikowanymi i trudnymi do zrozumienia. Dodatkowo dekoratory używane w rzeczywistym kodzie często składają się z innych dekoratorów i rzadko występują samodzielnie.
Python Decorator Library jest dobrym źródłem dosyć prostych przykładów. Dekorator Synchronize jest jednym z prostszych. Przykład Easy Dump of Function Arguments jest bardziej kompleksową wersją naszego przykładu pokaz_wywolanie.
Źródła
What's new in Python 2.4 prezentuje skrótowy, nieformalny wstęp do dekoratorów.
PEP 318 jest formalną specyfikacją dekoratorów.
Python Decorator Library zawiera wiele przykładów użytecznych dekoratorów.
Dokumentacja do modułu decorator autorstwa Michele Simionato zawiera opis problemu tworzenia dobrych implementacji dekoratorów i wiele przykładów dekoratorów. Ten moduł dostarcza też sposobu na tworzenie dekoratorów, które zachowują nienaruszone sygnatury funkcji.
David Mertz na łamach poczytnej serii Charming Python zaprezentował artykuł Decorators make magic easy.
[1] | ang. First-class objects |
[2] | ang. wraper |
[3] | ang. syntactic sugar |
[4] | ang. callable |