Udekorowane funkcje

2010-09-22 16:46

Kilkanaście dni temu opisywałem błąd, który – jak się okazało – był efektem ubocznym pewnej cechy języka Python, uznanej przeze mnie za niemal zupełnie niepotrzebną. Dla równowagi więc dzisiaj przedstawię feature, który wydaje się być bardzo przydatny – żeby nie było, że potrafię tylko krytykować ;-)

Prawdopodobnie najlepiej jest pokazać go na prostym, acz obrazowym przykładzie. Miejmy pewną funkcję na tyle dla nas istotną, że chcemy logować wszystkie jej wywołania. Wydaje się, że nic prostszego:

  1. import logging
  2.  
  3. def vif(): # Very Important Function :)
  4.     logging.debug("Called function vif()")
  5.     # ...

Ciężko jednak nazwać takie rozwiązanie uniwersalnym. Nie chodzi tu jedynie o jawnie wpisaną nazwę funkcji, ale raczej o konieczność dodawania logging.debug(...) na początku każdej funkcji, jaką chcemy monitorować. Sprawa komplikuje się jeszcze bardziej, gdy interesują nas też wyjścia z funkcji; wówczas jedynym wyjściem jest chyba opakowanie całej treści w jeden wielki blok tryfinally. Rezultat na pewno nie będzie piękny :)

I tutaj właśnie z pomocą przychodzą dekoratory – ciekawa opcja języka Python, na pierwszy rzut oka przypominająca adnotacje z Javy. Podobieństwo jest jednak głównie składniowe. Udekorowana wersja naszej ważnej funkcji wygląda bowiem tak:

  1. @trace
  2. def vif():
  3.     # ...

Znak @ poprzedza tutaj nazwę dekoratora, czyli trace (ang. śledź – i bynajmniej nie chodzi o rybę ;]). Czym jednak jest ów dekorator? Otóż on sam również jest funkcją, mogącą wyglądać choćby tak:

  1. import logging
  2.  
  3. def trace(func):
  4.     def _func(*args, **kwargs):
  5.         logging.debug("Calling function %s()", func.__name__)
  6.         return func(*args, **kwargs)
  7.  
  8.     return _func

Jej jedynym argumentem jest w założeniu funkcja, zaś rezultatem wywołania jest… również funkcja :) A zatem nasz dekorator potrafi przekształcić jedną funkcję w drugą, a dokładniej w jej udekorowaną, “opakowaną” wersję. To opakowanie definiowane jest wewnątrz dekoratora i polega, jak widać, na poprzedzeniu wywołania oryginalnej funkcji zapisem do loga.
Oczywiście za to, aby wywołanie funkcji opatrzonej dekoratorem było tak naprawdę odwołaniem do jej udekorowanej wersji odpowiada już sam język. Nie jest to zresztą skomplikowane, bo w istocie cały mechanizm jest tylko cukierkiem składniowym. Jest to jednak bardzo smaczny cukierek, który potrafi wydatnie podnieść czytelność kodu – jeśli stosuje się go właściwie.

Do czego jednak – oprócz logowania wywołań – dekoratory mogą się przydać? Ano przede wszystkim do upewniania się, że pewne konieczne warunki wstępne dla funkcji są spełnione na jej wejściu (i ewentualnie wyjściu). Może to być na przykład:

  • @connected – sprawdzenie połączenia z serwerem zanim spróbujemy wymieniać z nim dane i nawiązanie go w razie potrzeby
  • @authorized – określenie uprawnień wymaganych u aktualnie zalogowanego użytkownika przed wywołaniem funkcji wykonującej potencjalnie niebezpieczną operację
  • @synchronized – zabezpieczenie wywołania funkcji semaforem lub sekcją krytyczną

Wspólną cechą takich dekoratorów jest to, że są one swoistymi pseudodeklaracjami, nieodległymi koncepcyjnie zbyt daleko od komentarzy w rodzaju:

  1. # Ta funkcja wymaga połączenia z serwerem!
  2. def do_something():
  3.     # ...

Ich przewagą jest jednak rzeczywiste sprawdzanie, czy wymagania zostały spełnione – i to w sposób automatyczny i przezroczysty. Według mnie to właśnie stanowi o ich sporej przydatności.

Tags: , ,
Author: Xion, posted under Programming »


9 comments for post “Udekorowane funkcje”.
  1. MSM:
    September 22nd, 2010 o 16:58

    Nie zostało napisane (jedynie wspomniane), więc mam okazję do pochwalenia się wiedzą ;)

    Tak naprawdę zapis
    @decorator
    def fx(): ...

    Jest dokładnie równoważny zapisowi
    def fx() ...
    decorator(fx)

    Opcja dekoratora została wprowadzona do nowszych wersji Pythona właśnie m.in. ze względu na metody statyczne – wcześniej wymagało to nieintuicyjnego zapisu staticmethod(_metoda) po deklaracji funkcji.

  2. Xion:
    September 22nd, 2010 o 23:32

    @MSM: Prawie. Rezultat wywołania dekoratora na funkcji trzeba jeszcze przypisać do jej pierwotnej nazwy:

    1. def fx() ...
    2. fx = decorator(fx)

    W tej wersji jednak musi to nastąpić oczywiście po treści funkcji, więc nie jest to już tak opisowe (i łatwiej można takie przypisanie przeoczyć).

  3. Liosan:
    September 23rd, 2010 o 11:21

    I czym to się różni od anotacji w Javie? Moim zdaniem to różnice, a nie podobieństwa, są składniowe :) W Javie jak zwykle trzeba więcej pisać, ale combo anotacja + pointcut + aspect realizuje wszystko to co opisałeś i jeszcze trochę. I to nawet jest dość znana rzecz – http://en.wikipedia.org/wiki/Aspect-oriented_programming

  4. Xion:
    September 24th, 2010 o 14:18

    Dekoratory zasadniczo realizują część programowania aspektowego, więc nie dziwi mnie specjalnie, że znajdują odpowiedniki w innych językach :)
    Ale same adnotacje w Javie to – o ile wiem – tylko statyczne znaczniki, które zawierają ewentualnie jakieś tam parametry i są możliwe do odczytania i interpretacji przez JVM (lub kompilator, jak w przypadku @Override). Są raczej bliżej atrybutów w .NET niż dekoratorów w Pythonie.

  5. Liosan:
    September 27th, 2010 o 12:59

    Jasne – wszystko racja. Dlatego wspomniałem jeszcze o pointcutach i aspectach :)

    A, jak już porównujemy, to te takie dekoratory potrafią przyjmować parametry?
    @Authorized(role = Admin, logIfUnauthorized = false) ?

  6. Xion:
    September 27th, 2010 o 14:55

    Tak, ale wtedy wygodniej jest dekorator zdefiniować jako klasę. W konstruktorze przyjmuje ona parametry dla dekoratora, zaś przeciążony operator () (w Pythonie funkcja __call__) przyjmuje funkcję do udekorowania. Przykład:

    1. class Authorized(object):
    2.     def __init__(self, role, logIfUnauthorized):
    3.         self.role = role
    4.         self.log = logIfUnauthorized
    5.  
    6.     def __call__(self, f):
    7.         def _f(*args, **kwargs):
    8.             # Logika logowania i sprawdzania roli
    9.             f(*args, **kwargs)
    10.         return _f

    (Można to też zrobić funkcją, ale składniowo wychodzi wtedy potworek w postaci dwukrotnie zagnieżdżonych funkcji wewnętrznych, więcej tutaj).

  7. Kos:
    October 21st, 2010 o 11:31

    @up – Tak, zgodzę się że dwukrotnie zagnieżdżone funkcje (wychodzące w najgorszym wypadku) to nieco zamotana rzecz, ale jest dość logiczna, jeśli spojrzymy na dekorator w ogólnej postaci:

    @expr
    def func():
    # kod

    Tutaj ‘expr’ to dowolne wyrażenie, które jest obliczone raz w momencie deklaracji i ma zwrócić dekorator (czyli funkcję przyjmującą funkcję jako argument i zwracającą “przetworzoną” funkcję). Idąc tym tropem, z powyższego kodu wychodzi nam coś w stylu:

    decorator = expr
    def func():
    #kod
    func = decorator(func)

    Zwykle jako ‘expr’ podajemy po prostu obiekt funkcji, czyli sam dekorator.

    Jeśli chcemy parametry, to nic prostszego, tyle, że zamiast obiektu funkcji-dekoratora musimy podać cały function call, który [b]zwróci[/b] dekorator. Stąd dodatkowe zagnieżdżenie – piszemy nie tylko dekorator, który zwraca funkcję, ale dodatkową funkcję z dowolnymi parametrami, która zwraca dekorator, który z kolei przyjmuje zwraca funkcję :). I prawdą jest, że często wychodzi z tego właśnie podwójnie zagnieżdżona funkcja.

    Niemniej pisanie czegoś takiego wcale nie jest skomplikowane, gdy sobie uświadomimy, że mamy po prostu do zrobienia coś, co z takiej składni:

    @foo(a,b)
    def func():
    #kod

    zadziała tak:

    decorator = foo(a,b)
    def func():
    #kod
    func = decorator(func)

    Zwykle wolę to niecne rozwiązanie z podwójnie zagnieżdżoną funkcją, niż z klasą, choć w powyższym kodzie widać, że wychodzi na to samo.

    Swoją drogą, niektóre dekoratory w ogóle nie potrzebują zagnieżdżonych funkcji, np. taki, którego użyłem w jakiejś aplikacji konsolowej do obsługi command line:

    def command(func):
    global commands #slownik string->funkcja
    commands[func.func_name] = func
    return func

    W ten sposób (+ nieco kodu do czytania parametrów) jedną adnotacją mogłem uczynić każdą funkcję dostępną z poziomu linii komend. Dekoratory są proste i przyjemne :).

  8. Kos:
    October 21st, 2010 o 11:33

    (Argh! Wycięło mi leading wcięcia wewnątrz !)

  9. Xion:
    October 21st, 2010 o 14:02

    “Dekoratory są proste i przyjemne :)”
    I to właśnie najważniejszy wniosek z tych wypocin :)

Comments are disabled.
 


© 2018 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.