Dekoratory

Jest to swobodne tłumaczenie artykułu autorstwa Kenta S. Johnsona.

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)
foo

A 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 komunikat

Jeś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():
    pass

jest 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 opakowanie

W 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 komunikat

Ale 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_nrazy

Tutaj 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