
Dekoratory - wybawienie Pythona
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.
Zobacz kursy online
-
(69,65 zł najniższa cena z 30 dni)
89.54 zł199.00 zł (-55%) -
(62,65 zł najniższa cena z 30 dni)
98.45 zł179.00 zł (-45%) -
(59,70 zł najniższa cena z 30 dni)
89.54 zł199.00 zł (-55%) -
(44,70 zł najniższa cena z 30 dni)
67.05 zł149.00 zł (-55%) -
(45,15 zł najniższa cena z 30 dni)
64.50 zł129.00 zł (-50%) -
(59,60 zł najniższa cena z 30 dni)
67.05 zł149.00 zł (-55%) -
(34,65 zł najniższa cena z 30 dni)
44.55 zł99.00 zł (-55%) -
(38,70 zł najniższa cena z 30 dni)
58.04 zł129.00 zł (-55%)