Podobnie jak rangę ptaków w środowisku naturalnym podnosi ich przedstawiciel - jerzyk - będący swego rodzaju wybawcą ludzkości od nadmiaru owadów, tak dekoratory są niezwykłym przedstawicielem mechanizmów Pythona. Dekorator to (najczęściej) funkcja przyjmująca za swój pierwszy parametr (zwykle) inną funkcję. Ściśle rzecz ujmując w obu przypadkach może to być dowolny obiekt wywoływalny. I tak najprostszym dekoratorem może być:
 


def do_twice(func): 
    func()
    func()


Możemy z niego skorzystać przy dowolnej funkcji, na przykład wyświetlającej "Hello World!"



@do_twice
def print_hello_world():
    print("Hello World!")
 

Gdy jednak uruchomimy nasz program okaże się, że funkcja print_hello_world zostanie wywołana bez naszej wiedzy, świadomości, chęci i moralnego przyzwolenia. Dzieje się tak dlatego, że zapis z małpą (tuż nad funkcją) jest wywołaniem dekoratora, a nie jego deklaracją. Stąd dekoratory zazwyczaj tworzą nową funkcję, którą zwracają, a którą możemy my sami, w odpowiednim dla nas momencie, wywołać. Poprawiony do_twice wygląda zatem tak:



def do_twice(func):
    def inner_function():
        func()
        func()
    return inner_function
  

W ten sposób "@do_twice" choć zostaje wywołany, to jego wywołanie jedynie co robi, to tworzy nową funkcję, bez jej wywoływania. Aby wywołać udekorowana funkcję wystarczy wywołać jej pierwotną nazwę, czyli print_hello_world(). Szybko jednak zauważymy, że dekorator do_twice ma dużo wad. Wypiszmy je: * Dekorator do_twice nie zapamiętuje wyniku naszej udekorowanej funkcji ani go nam nie zwraca (jeżeli nasza funkcja ma "return" to gubimy tę informację) * Jeśli wypiszemy nazwę naszej funkcji, a mianowicie print(print_hello_world.__name__) to okaże się, że jest nią teraz inner_function, a przecież powinno pozostać print_hello_world * Jeżeli funkcja (udekorowywana) posiada jakieś argumenty to jak dotąd w żaden sposób ich nie przekazaliśmy Każdy z tych punktów ma swoje rozwiązanie i z tego powodu prawidłowym "szkieletem" dekoratora jest poniższy:



from functools import wraps
 
def decorator(func):
    @wraps(func)
    def inner_function(*args, **kwargs):
        # wszystko co chcemy zrobić przed wywołaniem funkcji
        result = func(*args, **kwargs)
        # wszystko co chcemy zrobić po wywołaniu funkcji
        return result
    return inner_function


I tak nasz do_twice powinien wyglądać w następujący sposób:



from functools import wraps
 
def do_twice(func):
    @wraps(func)
    def inner_function(*args, **kwargs):
        func(*args, **kwargs)
        result = func(*args, **kwargs)
        return result
    return inner_function


teraz możemy dekorator do_twice zastosować do dowolnej funkcji bez względu czy ta coś zwraca czy też nie lub czy przyjmuje argumenty i ile ich przyjmuje. Omówmy rozwiązania naszych problemów: * result = func(*args, **kwargs) wraz z return result powoduje brak efektu Alzheimera (zapominania wyniku) * samo przekazanie *args i **kwargs powoduje, że dekorator może zostać dodany do funkcji przyjmującej dowolną ilość argumentów pozycyjnych (args) jak i dowolną ilość argumentów słownikowych (kwargs) * wraps z biblioteki functools sam w sobie jest dekoratorem i powoduje przepisanie atrybutów z udokorowywanej funkcji do inner_function (atrybutów takich jak __name__) przez co podmiana jest bardziej "smukła". Pozostaje teraz zapoznać się z przykładem jakiegoś (bardziej) użytecznego dekoratora. Załóżmy, że mamy trzy funkcje:



def sum_up_to(n):
    summ = 0
    for i in range(n + 1):
        summ += i
    return summ


def triple_power(n):
    return n**n**n % (n + 100)


def repeat_number(n, repeat=1):
    return int(str(n)*repeat)

 

i chcielibyśmy sprawdzić jak długo działa każda z nich przy każdym wywołaniu z różnymi parametrami. Do tego świetnie będzie nadawał się dekorator measure_time, który właśnie napiszemy:



from functools import wraps
from time import perf_counter
 
def measure_time(func):
    @wraps(func)
    def inner_function(*args, **kwargs):
        start = perf_counter()
        result = func(*args, **kwargs)
        stop = perf_counter()
        print(f"Funkcja wykonała się w {stop - start:.2f}s")
        return result
    return inner_function
 

teraz zmieniamy nasze funkcje dodając im jedną linijkę przed każdą z definicji:



@measure_time
def sum_up_to(n):
    summ = 0
    for i in range(n + 1):
        summ += i
    return summ


@measure_time
def triple_power(n):
    return n**n**n % (n + 100)


@measure_time
def repeat_number(n, repeat=1):
    return int(str(n)*repeat)
 

i tak o to przy uruchomieniu każdej z nich otrzymamy (prócz wyniku) czas w jakim funkcja się wykonała. Na moim komputerze dla wywołań:



print(sum_up_to(10**8))
print(triple_power(7))
print(repeat_number(123456789, 10))


było to:
 


Funkcja wykonała się w 3.07s
5000000050000000
Funkcja wykonała się w 0.12s
45
Funkcja wykonała się w 0.00s
123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789
 

Uwaga: Dla Pythona 3.10 i nowszych warto ustawić: import sys



sys.set_int_max_str_digits(0)
 

by aplikacje obsługujące liczby o długości większej niż 4300 cyfr były poprawnie obsługiwane.