Dekoratory część 2

Mała rekapitulacja

Nasze poprzednie spotkanie z dekoratorami zakończyliśmy niemiłym akcentem. Nasz zrobiony najprostszym sposobem cache działał co prawda i tworzone przez niego obiekty robiły to, co od nich oczekiwaliśmy, jednak nie przebiegało to wszystko całkowicie bezszwowo. Udekorowana funkcja nazywała się inaczej i posiadała inną sygnaturę (zbiór argumentów, który według dokumentacji przyjmowała). Dodatkowo gdybyśmy spróbowali ją udokumentować, okazałoby się, że również jej docstring nie byłby zachowany.

Kto nam pomoże?

Rzeczywiście zadbanie o to wszystko przy budowie każdego dekoratora to dużo roboty. Skoro jednak robota ta jest w gruncie rzeczy powtarzalna i identyczna dla każdego dobrze sprawującego się dekoratora, to możemy się spodziewać, że ktoś już za nią ją odwalił. Tym kimś byli twórcy biblioteki decorator zawierającej dekorator @decorator.

Napisanie ostatniego zdania sprawiło mi, muszę się przyznać dużą frajdę. Jeśli jednak nadal będę w ten sposób opisywał funkcjonalność biblioteki, zrozumiałość wpisu zbliży się do zera. Dlatego przejdę od razu do przykładu:

from decorator import decorator
from time import sleep

def cache_caller(fun, *args):
    if fun.cache.has_key(args):
        return fun.cache[args]
    else:
        result = fun(*args)
        fun.cache[args] = result
        return result

def cache(fun):
    fun.cache = {}
    return decorator(cache_caller, fun)

@cache
def dummy(x):
"""These goggles do nothing"""
    sleep(1)
    return x

Stworzona przez powyższy kod funkcja dummy po wywołaniu będzie zachowywać się będzie tak samo, jak jej odpowiednik z poprzedniego wpisu. Różnicę jednak zauważymy, gdy wywołamy polecenie help(dummy). Temu helpowi nie możemy już niczego zarzucić: funkcja ma taką nazwę, z jaką ją zdefiniowaliśmy, przyjmuje takie same argumenty (ma tę samą sygnaturę) i zachowuje docstring. O wszystko to zadbała biblioteka.

Napisałem powyżej, że funkcja decorator jest dekoratorem. W przykładzie powyższym jednak tego nie widzimy. Nie korzystamy tam ze składni z @, a funkcję wywołujemy z dwoma argumentami. Pierwszy to caller, czyli funkcja, która wywoła nam funkcję udekorowaną. Będzie on wywoływany za każdym razem, gdy będziemy chcieli wywołać funkcję dummy. Drugi to wrapper, który czyni z naszej funkcji dekorator. Ten wywołany zostanie tylko raz dla każdej dekorowanej funkcji. To w nim zawarte jest przypisanie do funkcji pustego słownika cache.

decorator jako dekorator

Możliwość korzystania z funkcji decorator, jako dekoratora widzimy wtedy, gdy wrapper jest trywialny i nie zawiera żadnej logiki oprócz "udekorowania" funckji. Załóżmy, że mamy grupę funkcji "niebezpiecznych". Przed wywołaniem każdej z nich chcemy zapytać użytkownika o pozwolenie. Według schematu powyżej wyglądałoby to tak:

from decorator import decorator

def confirm_caller(fun, *args, **kwargs):
    prompt = 'Próbujesz wywołać funckję %s. Czy jesteś pewien (t/n): ' %\
            fun.func_name
    while True:
        ans = raw_input(prompt)
        if ans.lower() in ['t', 'n']:
            break
        prompt = 'Niepoprawna odpowiedź! '
    if ans.lower() == 't':
        return fun(*args, **kwargs)
    else:
        print "Odwołano"

def confirm(fun):
    return decorator(confirm_caller, fun)

@confirm
def dangerous():
    print "Zepsułeś komputer!!!"

if __name__ == '__main__':
    dangerous()

Wywołanie funkcji dangerous spowoduje wyświetlenie ostrzeżenia i tylko jeśli użytkownik potwierdzi, jej ciało zostanie wykonane. Zwróćmy jednak uwagę na wrappera funkcji. Praktycznie identyczny będzie on w każdym przypadku, gdy nie ma żadnej dodatkowej logiki dla tworzeniu zmodyfikowanej funkcji (a tylko przy jej wywoływaniu). Dlatego biblioteka oferuje nam prostszy sposób osiągnięcia tego samego. Wystarczy:

  1. Usunąć z powyższego kodu funkcję confirm
  2. Przemianować callera tak, by nazywał się jak żądany dekorator (czyli w naszym przypadku confirm)
  3. Użyć funkcji decorator jako dekoratora dla confirm.

Po tej zmianie kod działa identycznie, a udało nam się pozbyć trywialnej i utrudniającej czytanie funkcji.

Podsumowując

Biblioteka decorator znacznie ułatwia nam tworzenie nowych dekoratorów przez:

  1. Zastąpienie notacji zagnieżdżonej notacją płaską.
  2. Zachowanie wszystkich cech dekorowanej funkcji takich jak docstring, sygnatura i nazwa.

Funkcję decorator wywołać możemy:

  1. Z dwoma argumentami, z których pierwszy to caller (funkcja wywołująca dekorowaną i zwracająca jej (być może zmieniony) wynik), a drugim funkcja wrappowana. Jeśli napiszemy funkcję wrapper przyjmującą funkcję jako argument i wołającą dla niej funkcję decorator, to taki wrapper może być użyty jako dekorator.
  2. Jako dekorator dla callera. Wówczas trywialny wrapper zostanie dla nas wygenerowany, a dekorator, który uzyskamy będzie nazywał się tak jak caller.

Ostatni wpis o dekoratorach będzie krótki. Powiem trochę o dekoratorach z dodatkowymi argumentami, dekorowaniu metod i o tym, na co należy uważać.

Meta

Published: Lis 24, 2010

Author: zefciu

Comments:  

Word Count: 696

Next: Społecznościówka w 3 krokach - krok 1

Previous: Wyginanie pythona

Bookmark and Share

Tags

biblioteki dekoratory metaprogramowanie

Follows Up On

Comments powered by Disqus