TI/Programowanie dla Fizyków Medycznych:Dekoratory

Z Brain-wiki

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

Zadanie 0

Napisz dekorator, który spowoduje, że przy wywołaniu udekorowanej funkcji wypisze się na ekran informacja po raz który ta funkcja jest wywoływana, np.: "Funkcja <nazwa funkcji> została uruchomiona <n> raz"

Zadanie 1

Napisz dekorator przechowujący w pamięci wynik wywołania funkcji z jakimś parametrem, tak aby gdy powtórnie wywołamy ją z tym samym parametrem, funkcja nie była wywoływana powtórnie, ale by zwracana była wartość jaką otrzymano wcześniej przy wywołaniu z tym parametrem.

Zadanie 2

Napisz dekorator do funkcji zwracającej wartość logiczną wywołujący funkcję do czasu gdy zwróci True. Przetestuj na funkcji czytającej z wejścia i sprawdzającej czy użytkownik wpisał 'python'. Niech dekorator jako argument przyjmuje liczbę całkowitą oznaczającą maksymalną liczbę prób wywołania funkcji dekorowanej.

Zadanie 3

Napisz dekorator, który będzie wymagał podania hasła przed właściwym wywołaniem funkcji, jeśli zostanie podane błędne hasło to niech będzie wypisany komunikat o braku dostępu.

Zadanie 4

Napisz dekorator rejestrujący funkcje do listy będącej parametrem dekoratora, a następnie wywołaj wszystkie funkcje z listy


"Programowanie dla Fizyków Medycznych"