W Pythonie dekorator funkcji wyższego rzędu to funkcja, która przyjmuje inną funkcję (lub klasę) i zwraca nową funkcję (lub nową zmodyfikowaną klasę). Dekoratory często używane są do realizacji wzorca "owijania" (wrapping), który pozwala na dodanie dodatkowej logiki (na przykład logowania, buforowania, weryfikacji uprawnień itp.) do już istniejących funkcji bez zmiany ich kodu źródłowego.
Aby zachować nazwę, dokumentację i inne metadane oryginalnej funkcji, zaleca się użycie funkcji functools.wraps:
import functools def log_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f'Calling {func.__name__}') return func(*args, **kwargs) return wrapper @log_decorator def add(a, b): """Dodaj dwie liczby""" return a + b print(add(2, 3)) # Wynik: Calling add 5 print(add.__name__) # Wynik: add print(add.__doc__) # Wynik: Dodaj dwie liczby
Kluczowa kwestia: bez functools.wraps, owinięta funkcja straci nazwę, dokumentację i inne metadane oryginału, co negatywnie wpływa na debugowanie i autoodokumentację.
Co się stanie, jeśli owiniemy funkcję bez użycia functools.wraps, jakie będą atrybuty name i doc funkcji?
Odpowiedź: Będą one dziedziczone od wewnętrznej funkcji-owijki (zwykle 'wrapper'), przez co stracisz oryginalne metadane.
def simple_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @simple_decorator def f(): """To jest docstring""" pass print(f.__name__) # Wynik: 'wrapper' (NIE 'f') print(f.__doc__) # Wynik: None (a nie 'To jest docstring')
Historia
W projekcie zrealizowano skomplikowany system dekoratorów do logowania punktów końcowych API, ale nie zastosowano functools.wraps. W rezultacie, autogeneracja dokumentacji (Swagger/OpenAPI) oraz narzędzia introspekcyjne pokazywały nazwy wszystkich punktów końcowych jako 'wrapper', a dokumentacja zniknęła. Utrudniało to bardzo wsparcie, testowanie i konserwację.
Historia
Podczas pisania testów jednostkowych z użyciem pytest wystąpił błąd odkrywania testów: funkcje testowe, które były dekorowane swoimi dekoratorami bez wraps, nie były wykrywane, ponieważ ich name był niepoprawny. Powód — pytest szuka funkcji według nazwy.
Historia
Podczas śledzenia stosu wyjątków (traceback) ślad stosu zawsze wskazywał na "wrapper", co uniemożliwiało zrozumienie, która dokładnie funkcja wywołała błąd, ponieważ metadane korzenia zostały utracone z powodu braku functools.wraps w użytkownikowych dekoratorach.