programowanieProgramista Backend w Pythonie

Czym są dekoratory funkcji w Pythonie, jaka jest ich historia i do czego są stosowane we współczesnym programowaniu?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

Historia pytania

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.

Problem

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.

Rozwiązanie

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:

  • Pozwalają ponownie używać kod (zasada DRY);
  • Mogą być stosowane dla funkcji, metod, klas;
  • Pozwalają centralnie wdrażać zadania krzyżowe (logowanie, cache, dostęp, profilowanie);

Pytania z pułapką.

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

Typowe błędy i antywzorce

  • Ignorowanie functools.wraps traci meta-informacje o oryginalnej funkcji;
  • Logika dekoratora nie uwzględnia wyjątków, nie można uchwycić i przetworzyć błędu wewnątrz;
  • Niezrozumiałe nakładanie się kilku dekoratorów

Przykład z życia

Negatywny przypadek

Logowanie czasu wykonania dodane do 10 funkcji ręcznie, przez kopiowanie-kodowanie.

Zalety:

  • Logika jasna, kod blisko, łatwo znaleźć błąd.

Wady:

  • Trudno utrzymać — będzie trzeba zmieniać dziesiątki miejsc, jeśli trzeba zmienić zachowanie.
  • Kod się dubluje, narusza zasadę DRY.

Pozytywny przypadek

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:

  • Zmiany są wprowadzane centralnie;
  • Kod jest krótszy i czytelniejszy.

Wady:

  • Możliwa utrata informacji o sygnaturze bez functools.wraps, jeśli nie jest się uważnym;
  • Nowicjuszom może być trudniej od razu zrozumieć mechanizm działania dekoratorów.