Historia pytania
Dekoratory to jeden z najpotężniejszych narzędzi w Pythonie, pozwalający modyfikować zachowanie funkcji lub metod. Czasami trzeba nie tylko "owinąć" funkcję, ale również skonfigurować dekorator za pomocą parametrów (argumentów). Takie przypadki występują przy logowaniu, pomiarze czasu, ograniczeniach dostępu itp.
Problem
Zwykłe dekoratory przyjmują tylko jedną funkcję, którą trzeba owinąć. Kiedy trzeba przekazać samego dekoratorowi parametry, składnia staje się bardziej złożona, co często prowadzi do błędów, zwłaszcza przy zagnieżdżaniu funkcji i przekazywaniu *args/**kwargs.
Rozwiązanie
Dekorator z parametrami realizuje się za pomocą funkcji wyższego rzędu: najpierw wywoływana jest zewnętrzna "dekorująca" funkcja z argumentami, która tworzy i zwraca sam dekorator, a dekorator już otrzymuje funkcję i zwraca owinięcie:
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 greet(name): print(f"Hello, {name}!") greet("Python") # Wynik: Hello, Python! (3 razy)
Kluczowe cechy:
Dlaczego nie można zrealizować dekoratora z parametrami tak samo, jak zwykłego dekoratora?
Jeśli stosujesz @decorator, Python przekazuje funkcję jako argument do dekoratora. Dodając nawiasy (@decorator()), Python najpierw wywołuje funkcję, a dopiero jej wynik jest interpretowany jako dekorator.
def deco(func): # zwykły dekorator: @deco def deco_with_args(arg): # dekorator z argumentem: @deco_with_args(arg)
Czym zasadniczo różnią się dekoratory z argumentami od dekoratorów bez argumentów na poziomie wywołania?
Dekoratory bez argumentów przyjmują jako wejście funkcję, dekoratory z argumentami — nie funkcję, a parametry i zwracają dekorator.
Jak poprawnie stosować functools.wraps i po co to robić?
functools.wraps(func) w owinięciu zapisuje nazwę, dokumentację i inne metadane oryginalnej funkcji, w przeciwnym razie wszystkie te informacje zostaną zastąpione w wrapperze, co utrudnia debugowanie i introspekcję.
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
Zrealizowano dekorator bez uwzględnienia parametrów lub z niewłaściwą liczbą zagnieżdżonych funkcji:
def log(level): def wrapper(func): # błąd — wrapper powinien być głębiej print(f"Log: {level}") func() # Funkcja nie jest zwracana jako dekorator return wrapper @log("INFO") def action(): print("Work!")
Zalety:
Wady:
Użycie functools.wraps i poprawnych zagnieżdżonych funkcji:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
Zalety:
Wady: