Zapisz jako PDF

Transkrypt

Zapisz jako PDF
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.
Spis treści
1 Składnia
2 Klasyczne zastosowanie dekoratorów: metody statyczne i klasowe
3 Dekoratory definiowane jako funkcje czy klasy
4 Dekorator typu trace
5 Dekoratora zwracające oryginalną funkcję i dekoratory zwracające inny obiekt
6 Do czego (i czy wogóle) warto używać dekoratorów?
7 Przydatne linki
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):
...
funkcja.xxx = True
...
return funkcja
>>> @ustaw_atrybut_xxx
... def f():
...
print "ciao"
>>> f()
dekorator
ciao
>>> f.xxx
dekorator
True
# definicja naszego dekoratora
# wywołanie dekoratora
# dekorowany obiekt
# wywołanie obiektu zwróconego przez
# dodatkowy atrybut ustawiony przez
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)
poprzednim przykładzie
>>> f()
ciao
>>> f.xxx
True
# równoważne użyciu @ustaw_atrybut_xxx w
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
Python Decorator Library
moduł functools
Prezentacja o dekoratorach, wyjątkach i context managers
moduł decorator

Podobne dokumenty