Dekoratory pojawiły się w Pythonie jako syntaktyczny cukier od wersji 2.4, aby ułatwić pracę z funkcjami wyższego rzędu — takimi, które przyjmują lub zwracają inne funkcje. Ewolucja podejść do rozszerzania funkcjonalności funkcji doprowadziła do formatu zwięzłych i wyrazistych środków — adnotacji przez @decorator.
W dużych projektach często zachodzi potrzeba modyfikacji lub owijania funkcji dodatkową logiką: logowanie, sprawdzanie dostępu, cache’owanie, mierzenie czasu wykonania. Bez dekoratorów należało ręcznie wywoływać funkcje opakowujące, co powodowało rozrost kodu.
Dekoratory pozwalają wyciągać powtarzające się aspekty do osobnych opakowań, tym samym zwiększając czytelność i możliwość ponownego wykorzystania kodu. Dzięki @decorator można elegancko dodawać funkcjonalność do funkcji i metod:
Przykład kodu:
import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f'Elapsed: {time.time() - start:.3f}s') return result return wrapper @timing_decorator def slow_function(): time.sleep(0.5) slow_function() # Wyświetli, ile zajęło wykonanie
Kluczowe cechy:
Czy dekoratory mogą zmieniać sygnaturę opakowywanej funkcji?
Często błędnie uważa się, że sygnatura funkcji dekorowanej nie zmienia się. W rzeczywistości bez użycia modułu functools.wraps meta-informacje znikają, co może powodować nieoczekiwane błędy w automatycznej dokumentacji lub introspekcji.
Przykład kodu:
from functools import wraps def decorator(func): @wraps(func) def wrapper(*args): return func(*args) return wrapper
Czy dekoratory mogą być parametryzowane?
Często odpowiadają, że nie. W rzeczywistości możliwe jest stworzenie dekoratora parametryzowanego przez dodatkowy poziom zagnieżdżenia — funkcję zwracającą dekorator.
Przykład kodu:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print("Hello!")
Czy można stosować wiele dekoratorów do jednej funkcji? Jaki będzie porządek wykonania?
Często błędnie się zakłada, że porządek nie ma znaczenia lub że jest taki sam jak porządek umieszczenia dekoratorów w kodzie. W rzeczywistości najpierw stosowany jest najniższy dekorator, potem następny i tak dalej w górę.
Przykład kodu:
def dec1(f): def wrapper(*a, **k): print("dec1") return f(*a, **k) return wrapper def dec2(f): def wrapper(*a, **k): print("dec2") return f(*a, **k) return wrapper @dec1 @dec2 def f(): print("core") f() # dec1, dec2, core
Logowanie czasu wykonania dodane do 10 funkcji ręcznie, przez kopiowanie-kodowanie.
Zalety:
Wady:
Cała logika timingu wyciągnięta do dekoratora, wszystkie funkcje, w których potrzebna jest ta metryka, są owinięte dekoratorem @timing_decorator.
Zalety:
Wady:
functools.wraps, jeśli nie jest się uważnym;