TI/Programowanie dla Fizyków Medycznych:Dekoratory
Dekoratory w Pythonie służą do zastępowania zdefiniowanych przez nas funkcji przez funkcje (lub inne obiekty) zgodnie z definicją dekoratora. Na początku najlepiej zapoznać się z prostym przykładem dekoratora, który powoduje, że przed i po wywołaniu dekorowanej funkcji wypisywane są linie informujące o tym jaka funkcja jest wywoływana:
class decorator(object):
def __init__(self, f):
self.f = f
def __call__(self):
print 'przed wejściem do ', self.f.__name__
self.f()
print 'po wyjściu z ', self.f.__name__
@decorator
def g():
print 'wewnątrz g'
g()
Taki sam efekt można osiągnąć wpisując w ciało funkcji g dwie dodatkowe linijki, więc jakie są zalety wykorzystywania dekoratorów? Po pierwsze jeśli w podobny sposób chcemy zmodyfikować nie jedną ale wiele funkcji to napisanie dekoratora znacznie ułatwi to zadanie, ponadto dekorowanie funkcji jest łatwo zauważalne przez czytającego kod (dzięki @), pozwala na logiczne oddzielenie funkcjonalności na przykład właściwego działania funkcji od kodu realizującego logowanie użytkownika. Dekorowanie należy rozumieć jako składanie funkcji, kod z przykładu jest równoważny poniższemu:
class decorator(object):
def __init__(self, f):
self.f = f
def __call__(self):
print 'przed wejściem do ', self.f.__name__
self.f()
print 'po wyjściu z ', self.f.__name__
#@decorator
def g():
print 'wewnątrz g'
g = decorator(g)
g()
Dekoratory można definiować za pomocą klas lub funkcji, można dekorować funkcje mające argumenty lub nie, w końcu same dekoratory mogą przyjmować argumenty lub nie - omówimy teraz wszystkie te przypadki. Dekorator bezargumentowy zadany przez klasę musi mieć konstruktor przyjmujący jeden argument - funkcję, którą dekorujemy i implementować metodę __call__. Jeśli dekorujemy funkcję, która przyjmuje argumenty i chcemy aby funkcja po udekorowaniu przyjmowała takie same argumenty to metoda __call__ musi przyjmować takie argumenty:
class decorator(object):
def __init__(self, f):
self.f = f
def __call__(self, a, b, c):
print 'przed wejściem do ', self.f.__name__
self.f(a, b, c)
print 'po wyjściu z ', self.f.__name__
@decorator
def g(a, b, c):
print 'wewnątrz g: a =', a, 'b =', b, 'c =', c
g(1, 2, 3)
W tym przykładzie w czasie dekorowania zostanie wywołany konstruktor klasy decorator, a później przy wywołaniu funkcji g zostanie wywołana metoda __call__ klasy dekorator. Analogiczny dekorator zdefiniowany za pomocą funkcji musi być funkcją przyjmującą jeden parametr - funkcję, którą należy udekorować i zwracającą funkcję, którą funkcja dekorowana będzie zastąpiona:
def decorator(f):
def inner(a, b, c):
print 'przed wejściem do ', f.__name__
f(a, b, c)
print 'po wyjściu z ', f.__name__
return inner
@decorator
def g(a, b, c):
print 'wewnątrz g: a =', a, 'b =', b, 'c =', c
g(1, 2, 3)
Tym razem przy dekorowaniu zostanie wykonana funkcja decorator, a przy wywoływaniu funkcji g będzie wywoływana funkcja inner Kolejnym zagadnieniem jest tworzenie dekoratorów przyjmujących argumenty. Dekorator przyjmujący argumenty będący klasą musi mieć konstruktor przyjmujący dane argumenty i metodę __call__ przyjmującą dokładnie jeden argument - funkcję dekorowaną - i zwracającą funkcję, którą należy podstawić w miejsce dekorowanej:
class decorator(object):
def __init__(self, a):
self.a = a
def __call__(self, f):
def inner(a, b, c):
print 'parametr dekoratora a =', a
print 'przed wejściem do ', f.__name__
f(a, b, c)
print 'po wyjściu z ', f.__name__
return inner
@decorator(5)
def g(a, b, c):
print 'wewnątrz g: a =', a, 'b =', b, 'c =', c
g(1, 2, 3)
Tym razem w momencie dekorowania zostanie wywołany konstruktor klasy dekorator i metoda __call__ (będzie ona wywoływana tylko podczas dekorowania), a jej wynik zostanie przypisany na zmienną przechowującą dekorowaną funkcję. Przy wywołaniu funkcji g będzie wywoływana funkcja inner. Jeśli chcemy zrealizować analogiczną konstrukcję przy pomocy funkcji to dekorator musi być funkcją, która przyjmuje parametry dekoratora i zwraca funkcję, która przyjmuje dokładnie jeden argument - funkcję dekorowaną i zwraca funkcję, która ma być podstawiona w miejsce funkcji dekorowanej:
def decorator(a):
def wrapper(f):
def inner(a, b, c):
print 'parametr dekoratora a =', a
print 'przed wejściem do ', f.__name__
f(a, b, c)
print 'po wyjściu z ', f.__name__
return inner
return wrapper
@decorator(5)
def g(a, b, c):
print 'wewnątrz g: a =', a, 'b =', b, 'c =', c
g(1, 2, 3)
W tym przypadku podczas dekorowania zostanie wywołana funkcja decorator z argumentem 5 i funkcja wrapper z argumentem g, wywołując później funkcję g będziemy wywoływali funkcję inner. W Pythonie mamy dwa wbudowane dekoratory - classmethod i staticmethod - służą one do definiowania metod w klasach, które nie wymagają instancji klasy do ich wywołania - mogą być wywoływane przez klasę - metody udekorowane classmetod jako pierwszy parametr otrzymują klasę na rzecz której zostały wywołane, a staticmethod nie wiedzą nawet na rzecz jakiej klasy zostały wywołane:
class A(object):
def normalna(self):
print "metoda normalna, wywołana na obiekcie", self
@classmethod
def klasowa(cls):
print "metoda klasowa, wywołana na klasie", cls
@staticmethod
def statyczna():
print "metoda statyczna"
A.klasowa()
A.statyczna()
A().normalna()
Zauważ, że do wywołania metod statyczna() i klasowa() nie potrzebowaliśmy tworzyć obiektu klasy A, z kolei próba wywołania metody normalna() na klasie powoduje wyjątek:
>>> A.normalna()
Traceback (most recent call last):
File "<pyshell#195>", line 1, in <module>
A.normalna()
TypeError: unbound method normalna() must be called with A instance as first argument (got nothing instead)
Dekoratory można też wykorzystywać do modyfikowania klas, jeśli na przykład chcemy dodać zmienną klasową a i metodę b do klasy A możemy stworzyć następujący dekorator:
def decorator(C):
C.a = 5
def b(self):
print self
C.b = b
return C
@decorator
class A(object):
pass
a = A()
a.b()
print a.a
Funkcje i klasy można dekorować wieloma dekoratorami na raz:
def A(f):
def inner():
print 'dekorator A'
f()
return inner
def B(f):
def inner():
print 'dekorator B'
f()
return inner
@A
@B
def f():
print 'funkcja f'
f()