czwartek, 25 grudnia 2014

Dekorator w Python: Testowanie: Podmiana ciała funkcji

Z racji na wygodną składnię i elastyczność Python'a używanie dekoratorów jest potężnym środkiem, szczególnie w pisaniu aplikacji sieciowych, czy testowaniu oprogramowania. Chciałbym pokazać prosty przykład tego, jak wygodnie i elegancko podstawiać ciała funkcji na potrzeby testów.

Ogólna idea dekoratora w Python

Mamy funkcję bezargumentową o nazwie przywitaj_sie, która wypisuje na ekran i zwraca wartość prawdy. Chcąc objąć ją dekoratorem o nazwie dekorator (nazwa zmyślona), należy przy definicji użyć następującej notacji:
@dekorator
def przywitaj_sie():
    print 'Witaj'
    return True
Dekorator zazwyczaj jest funkcją, która ma zwrócić wywołalny obiekt, który podmieni przywitaj_sie. Definiując go nad funkcją powodujesz, że dekorator się wykonuje w chwili udekorowania funkcji przywitaj_sie a następnie zwraca wywoływalny obiekt podmieniający działanie funkcji przywitaj_sie. Dwa wnioski: dekorator (zazwyczaj funkcja) winien przyjmować jeden parametr, który jest wywoływalny; dwa: winien zwracać obiekt wywoływalny o takiej samej ilości parametrów jak funkcja, którą dekoruje (tu: zero).

W wielu przypadkach wręcz wskazane jest użycie domknięć. Definiując dekorator dla funkcji z przykładu powyżej można zrobić tak:

def dekorator(callme):
    def zamiennik():
        return False
    return zamiennik

Po udekorowaniu funkcji każde wywołanie funkcji przywitaj_sie zwraca wyłącznie False.

Dekorator może zwrócić swój argument, czyli teoretycznie nic nie zmieniać. To podejście stosowane jest szczególnie w wypadku mechanizmów rejestrowania wywołań zwrotnych (callbacks), gdyż dekorator jest uruchamiany podczas dekorowania każdej funkcji, a nie przy jej wywołaniu. Programowanie sieciowe notorycznie korzysta z tego mechanizmu np. do ustalania rejestrowania akcji na określony wzorzec URL albo rejestrowania wywołań zwrotnych (callback) na określony kod stanu HTTP.

Wracając do podstawień funkcji... Argumentem dekoratora może być cokolwiek, co jest wywoływalne, np. lambda. Dekorator musi zwracać cokolwiek wywoływalnego, może być również lambda. Jeszcze inna rzecz, że dekorator musi być wywoływalny (callable), co w wypadku Pythona oznacza bycie regularną funkcją, lambdą lub ... klasą ze zdefiniowaną metodą __call__().

Moja koncepcja podmiany wywołań funkcji (przydatne podczas testowania) to:

class Mocker(object):
    def __init__(self, substitute):
        assert(callable(substitute))
        self.substitute = substitute
    def __call__(self, mocked):
        assert(callable(mocked))
        return self.substitute
I przykłady zastosowania:
import random

@Mocker(lambda : random.random() > 0.5)
def zwroc_false():
    return False

for i in range(10):
    print zwroc_false()

@Mocker(lambda x,y : x*y)
def suma(a,b):
    return a+b

print suma(2,3)
print suma(3,4)

@Mocker(lambda s : s+'snake')
def podwoj_lancuch(lancuch):
    return s*2

print podwoj_lancuch('Ala')
print podwoj_lancuch('Alicja')