TI/Dekoratory

Z Brain-wiki

Dekoratory są w miarę ezoteryczną cechą Pythona — w przeciwieństwie do funkcji, klas czy iteratorów nie są powszechną cechą języków programowania. Niemniej, warto je omówić mimo wszystko, gdyż są niezwykle eleganckie i użyteczne.

Krótko mówiąc, dekorator to obiekt (np. funkcja), który można wywołać przekazując mu jako argument dekorowany obiekt. Tym dekorowanym obiektem może być funkcja lub klasa. Wartość zwrócona przez to wywołanie zostaje użyta zamiast dekorowanego obiektu.

Składnia

Dekoratora używa się wstawiając linijkę zaczynającą się od @ przed definicją dekorowanego obiektu (klasy czy funkcji).

Popatrzmy na przykład:

>>> def ustaw_atrybut_xxx(funkcja):  # definicja naszego dekoratora
...     funkcja.xxx = True
...     return funkcja
>>> @ustaw_atrybut_xxx               # wywołanie dekoratora
... def f():                         # dekorowany obiekt
...     print "ciao"
>>> f()                              # wywołanie obiektu zwróconego przez dekorator
ciao
>>> f.xxx                            # dodatkowy atrybut ustawiony przez dekorator
True

Definicja funkcji (zaczynająca się od def) tworzy funkcję, czyli obiekt typu function). Ten obiekt jest przepuszczany przez funkcję ustaw_atrybut_xxx, która dodaje dodatkowe pole. W rezultacie zastosowanie dekoratora powoduje, że pod nazwą f zostaje zachowana oryginalna funkcja, ale z dodatkowym atrybutem.

Składnia wywołania dekoratorów jest pomyślana tak, by trudno było przeoczyć fakt, że definiowany obiekt został dekorowany. Niemniej, zanim pojawiła się składnia z @, możliwe było użycie dekoratorów poprzez jawne wywołanie:

>>> def f():
...     print "ciao"
>>> f = ustaw_atrybut_xxx(f)   # równoważne użyciu @ustaw_atrybut_xxx w poprzednim przykładzie
>>> f()
ciao
>>> f.xxx
True

Ten przykład pokazuje też, co dokładnie powoduje wstawienie dekoratora — zachowanie pod nazwą definiowanej funkcji wartości zwracanej przez wywołanie dekoratora z oryginalną funkcja jako argumentem.

Klasyczne zastosowanie dekoratorów: metody statyczne i klasowe

Funkcje zdefiniowane w klasie tworzą metody. Ich pierwszy argument to zawsze self, magiczny parametr za który Python podstawia obiekt na którym wywoływana jest metoda. Niemniej, czasami wygodnie jest zdefiniować w klasie funkcję, którą można wywołać bez dostępu do instancji. Aby mieć taką możliwość, definiuje się metodę klasową (używając dekoratora classmethod) lub statyczną (używając dekoratora staticmethod). Różnica między nimi jest taka, że metoda klasowa ma pierwszy magiczny parametr tak samo jak zwykłe metody, tylko że Python wstawia za niego samą klasę, a nie jedną z jej instancji. Metoda statyczna zachowuje się jak zwykła funkcja, i lista argumentów jest przekazywana bez zmian.

>>> class T(object):
...     def a(self, x):
...         print self, x
...
...     @classmethod
...     def b(cls, x):
...         print cls, x
...
...     @staticmethod:
...     def c(x):
...         print x
>>> t = T()
>>> t.a(1)
<__main__.T object at ...> 1
>>> t.b(2)
<class '__main__.Test'> 2
>>> t.c(3)
3
>>> # nie możemy wywołać T.a()
>>> T.b(4)
<class '__main__.Test'> 4
>>> T.c(5)
5

Istotna różnica między metodą a i metodami b oraz c jest to, że te drugie można wywołać poprzez klasę, bez tworzenia instancji.

Nazwy pierwszego parametru metod a i b, czyli self i cls powinny być właśnie takie. Użycie nazwy self dla parametru nie będącego pierwszym parametrem zwykłej metody prowadzi do konfuzji osoby czytającej kod. Podobnie nazwanie takiego parametru jakkolwiek inaczej jest złamaniem reguł stylu. W przypadku parametru cls reguły nie są aż takie ścisłe, gdyż czasem nazywa się go klass, niemniej użycie słowa nie kojarzącego się od razu z klasą jest również błędem.

Dekoratory definiowane jako funkcje czy klasy

Zdefiniowany na początku dekorator ustaw_atrybut_xxx, mimo że wystarczający do zademonstrowania składni, raczej nie nie ma zastosowania praktycznego. Zanim przejdziemy do definiowania użytecznych dekoratorów, omówmy w jaki sposób można to zrobić. Na wstępnie zaznaczyliśmy, że dekorator to obiekt który można wywołać jak funkcję. Jako funkcja był też napisany nasz przykładowy dekorator ustaw_atrybut_xxx. Inny sposób na otrzymanie wywoływalnych obiektów to zdefiniowanie klasy z metodą __call__. Przewaga klasowego podejścia do dekoratorów nad funkcyjnym jest taka, że są one w ten sposób czytelniejsze. Definiowanie dekoratorów przy użyciu funkcji wymaga zastosowania dwu- lub trzykrotnie zagnieżdzonych funkcji. Przy definicji przez klasę analogiczna definicja zawiera po prostu dwie lub trzy metody. Dlatego też, w niniejszym omówieniu, dalsze dekoratory będziemy definiować jako klasy.

Na początek przypomnijmy, jak działa dodanie specjalnej metody __call__. Mianowicie, jeśli mamy obiekt, którego klasa definiuje __call__, to można go użyć jak funkcji i wywoła (dostawiając listę argumentów w nawiasach okrągłych).

>>> class T(object):
...     def __init__(self):
...         print "w __init__"
...     def __call__(self):
...         print "w __call__"
>>> t = T()
w __init__
>>> t()
w __call__

Przypomnijmy też, jak działa dodanie metody __init__. Mianowicie, zostaje ona wywołana automatycznie po tym, jak stworzymy nowy obiekt. Tworzenie obiektów w Pythonie przypomina zwykłe wywołanie funkcji, tyle tylko, że zamiast funkcji używa się klasy. Metoda __init__ nie zwraca nic.

Reasumując, operacja wywołania dokonana na klasie powoduje stworzenie nowego obiektu i wywołanie __init__, natomiast operacja wywołania dokonana na obiekcie powoduje wywołanie __call__.

Dla przykładu zdefiniujmy ustaw_atrybut_xxx na nowo, tym razem jako klasę.

>>> class ustaw_atrybut_xxx(object):
...     def __init__(self):
...         pass
...     def __call__(self, funkcja):
...         funkcja.xxx = True
...         return funkcja
>>> @ustaw_atrybut_xxx()
... def f():
...     return 'bonjour'
>>> f()
'bonjour'
>>> f.xxx
True

Nieco inny jest sposób wykorzystania dekoratora — o ile wcześniej po znaku @ następowała tylko nazwa funkcji, to teraz dostawiliśmy nawiasy, tak by skonstruować obiekt ustaw_atrybut_xxx.o

Dekorator typu trace

Zdefiniujmy użyteczny dekorator, który wypisze nazwę wykonywanej funkcji przed i po jej wykonaniu.

>>> class logged(object):
...     def __init__(self, funkcja):
...         self.funkcja = funkcja
...     def __call__(self, *args, **kwargs):
...         print "wywołanie", self.funkcja.__name__
...         x = self.funkcja(*args, **kwargs)
...         print self.funkcja.__name__, "zwróciła", x
...         return x
>>> @logged
... def f():
...     return g() + g()
>>> @logged
... def g():
...     return 1
>>> f()
wywołanie f
wywołanie g
g zwróciła 1
wywołanie g
g zwróciła 1
f zwróciła 2
2

Tutaj mamy taką sytuację, że nasz obiekt klasy dekorującej podstawiamy za dekorowany obiekt, i wywołanie f() czy g(), wywołuje naprawdę logged.__call__, a dopiero pośrednio oryginalną funkcję.

Dekoratora zwracające oryginalną funkcję i dekoratory zwracające inny obiekt

Dwa poprzednie przykłady różnią się w istotny sposób. Pierwszy dekorator, ustaw_atrybut_xxx, zwraca przekazany mu obiekt, lekko tylko go zmieniając. Natomiast drugi dekorator, logged, zwraca inny wywoływalny obiekt — instancję logged — który zostaje dowiązany pod nazwą oryginalnej funkcji.

Obie wersje są dozwolone, i obie wersje są użyteczne, w zależności od sytuacji. W pierwszej wersji działanie dekoratora ogranicza się do momentu definicji funkcji i wywołania dekoratora. Oznacza to, że dekorator może ustawić atrybut na funkcji, wypisać komunikat, czy dopisać funkcję do jakiegoś rejestru, ale nie może wpływać na sposób działania funkcji. Bardziej interesująca jest druga wersja, gdzie możliwe są nie tylko operacje w trakcie definicji funkcji, ale możliwe jest w zasadzie nieograniczone oddziaływanie na każde wywołanie funkcji. Przewagą pierwszej wersji nad drugą jest prostota i wydajność — nie tylko definicja dekoratora jest prostsza, ale również

  • lista funkcji widoczna w komunikacie o wyjątku nie zawiera funkcji dekorującej, której obecność może być niespodzianką dla użytkownika, zwłaszcza że często jest to anonimowa funkcja tworzona przy każdym wywołaniu dekoratora
  • działanie programu jest minimalnie szybsze, bo unikamy niewielkiego narzutu na każde wywołanie.

Do czego (i czy wogóle) warto używać dekoratorów?

Ponieważ już mniej więcej wiemy jak się definiuje dekoratory, pora odpowiedzieć na pytanie, czy i dlaczego warto je wykorzystywać. W przypadku logged, z całą pewnością moglibyśmy wstawić odpowiednie wyrażenia print na początek i koniec każdej z definiowanych funkcji. Wykorzystanie dekoratorów pozwala na rozdzielenie sfer odpowiedzialności pomiędzy właściwe funkcje (dokonujące "obliczeń") oraz funkcję wypisującą komunikaty. W przypadku mniej trywialnych funkcji powoduje to uproszczenie kodu i ułatwia jego zrozumienie. Zaletą jest też to, że unikamy duplikacji — zamiast identycznych poleceń print w każdej funkcji, piszemy je tylko raz, a następnie "wstawiamy", dodając jedną linijkę wywołującą dekorator.

Przykłady funkcjonalności jaką można zaimplementować w formie dekoratorów obejmuje

  • przechowywanie wykonanych wcześniej obliczeń w celu przyspieszenia działania programu,
  • oznaczanie funkcji jako "przestarzałych" (ang. deprecated) i wypisywania ostrzeżenia przy pierwszym wywołaniu,
  • pomiar czasu działania funkcji
  • i oczywiście wiele innych…

Przydatne linki