Dekorator to funkcja, która opakowuje funkcję
To zdanie brzmi abstrakcyjnie, ale mechanika jest prosta. Dekorator przyjmuje funkcję i zwraca funkcję. Zwracana funkcja zwykle wywołuje oryginał, a wokół tego wywołania dokłada jakieś dodatkowe zachowanie.
Najkrótszy możliwy przykład:
shout to dekorator. Przyjmuje funkcję (greet), buduje nową funkcję (wrapper), która wywołuje oryginał i zamienia wynik na wielkie litery, a potem ją zwraca. Przypisanie greet = shout(greet) podmienia oryginał na wersję opakowaną.
Ten wzorzec ponownego przypisania jest tak częsty, że Python dał mu osobną składnię.
@ to lukier składniowy dla ponownego przypisania
@name w linii nad def jest równoważne name = name(...) tuż po zdefiniowaniu funkcji:
@shout czyta się jako "zastosuj dekorator shout do tej funkcji". Python wykonuje greet = shout(greet) zaraz po def: ta sama mechanika co wcześniej, mniej pisania.
Gdy widzisz @name, w myślach zamień to na function = name(function). Tylko tyle znaczy ta składnia.
Obsługa argumentów
Większość funkcji przyjmuje argumenty. Użyteczny dekorator przekazuje je dalej. Idiom to *args, **kwargs, czyli pythonowy sposób na przyjęcie dowolnych argumentów, bo funkcję opakowującą nie powinno obchodzić, czego oczekuje opakowana funkcja:
*args przechwytuje wszystkie argumenty pozycyjne. **kwargs przechwytuje wszystkie argumenty nazwane. Funkcja opakowująca przekazuje wszystko bez zmian do opakowanej funkcji, a potem wykonuje dodatkową pracę, dla której istnieje dekorator: tutaj zamienia wynik na wielkie litery.
Tak wygląda większość prawdziwych dekoratorów.
Bardziej przydatny przykład: pomiar czasu
Wypisz, ile trwa wykonanie funkcji:
Ten wzorzec (zrób coś przed wywołaniem i coś po nim) to to, co ostatecznie robi większość dekoratorów. Logowanie, sprawdzanie uprawnień, ponawianie i walidacja wejścia mają ten sam kształt.
Zachowanie oryginalnej tożsamości: functools.wraps
Udekorowanie funkcji ją podmienia, więc opakowana funkcja traci swoje oryginalne atrybuty __name__ i __doc__:
greet.__name__ to teraz "wrapper", a docstring zniknął. To psuje help(), tracebacki i każde narzędzie, które analizuje funkcję.
Poprawka zajmuje jedną linię: @functools.wraps(func) na funkcji wewnętrznej kopiuje metadane.
Zawsze dodawaj @wraps(func) do funkcji wewnętrznej. Nic nie kosztuje, a oszczędza zaskakujących sesji debugowania w przyszłości.
Dekoratory z argumentami
Czasem sam dekorator potrzebuje konfiguracji: "ponów tę funkcję maksymalnie 3 razy", "loguj na poziomie DEBUG". To oznacza jeszcze jedną warstwę zagnieżdżenia: zewnętrzną funkcję, która przyjmuje argumenty i zwraca dekorator.
Trzy warstwy brzmią groźnie. Czytaj od zewnątrz:
repeat(times=3)to wywołanie funkcji. Zwracadecorator.decoratorto właściwy dekorator: przyjmuje funkcję i zwraca opakowaną.wrapperto opakowana funkcja, która działa w chwili wywołania.
Na tym kształcie opierają się @retry(times=5), @cache(maxsize=100) i dekoratory frameworków, takie jak @app.route("/users"). Gdy zobaczysz trójwarstwowy wzorzec, całą tę rodzinę czyta się tak samo.
Nakładanie dekoratorów
Na jedną funkcję możesz nałożyć więcej niż jeden dekorator. Nakładają się od dołu do góry: ten najbliżej def działa pierwszy:
add_exclaim opakowuje pierwszy i dodaje !. Potem shout opakowuje to wszystko i zamienia na wielkie litery. Wynik to HI, ROSA!.
Kolejność ma znaczenie. Po odwróceniu kolejności dostaniesz HI, ROSA! z wykrzyknikiem dodanym po zamianie na wielkie litery. Tutaj wygląda to identycznie, ale wyobraź sobie dekorator formatujący JSON: uruchomienie go przed albo po dekoratorze, który loguje wejście, może dać bardzo różne wyniki.
Wbudowane dekoratory, które zobaczysz
Python i jego biblioteka standardowa mają garść dekoratorów, które spotkasz w prawdziwym kodzie:
@propertyzamienia metodę w obliczany atrybut.@staticmethodoznacza metodę, która nie używaselfanicls.@classmethoddostaje klasę jakoclszamiast instancji, co świetnie sprawdza się w alternatywnych konstruktorach.@functools.lru_cachezapamiętuje wyniki, więc powtarzane wywołania z tymi samymi argumentami trafiają do cache'u.
Dekoratory frameworków (@app.route, @pytest.fixture, @dataclass) działają na tej samej zasadzie. Nic specjalnego: to tylko funkcje, które opakowują funkcje.
Kiedy pisać dekorator, a kiedy nie
Napisz dekorator, gdy chcesz zastosować to samo zachowanie do wielu funkcji: pomiar czasu, logowanie, ponawianie, sprawdzanie uprawnień. Cały sens polega na tym, że to zachowanie zostaje poza ciałem funkcji.
Odpuść dekorator, gdy:
- Zachowanie należy do jednej konkretnej funkcji. Umieść je w tej funkcji.
- Potrzebujesz go tylko do testów. Fixture albo parametr będzie czytelniejszy.
- Kusi cię nałożenie czterech czy pięciu dekoratorów. Wtedy przepływ sterowania chowa się w łańcuchu dekoratorów i czytelnik musi rozwinąć każdą warstwę, żeby zobaczyć, co naprawdę się wykonuje. Zwykła funkcja pomocnicza może czytać się lepiej.
Dekoratory to ostre narzędzie. Dobrze użyte utrzymują kod zgodny z DRY, a intencję oczywistą. Źle użyte ukrywają, co robi program. Decydując, czy po nie sięgnąć, stawiaj na "oczywiste".
Dalej: type hints
Dekoratory to częste miejsce, w którym spotyka się type hints w praktyce, bo funkcje wrapper często mają adnotowane sygnatury. Type hints to niewielka funkcja języka, która szybko się zwraca, i o nich jest następna strona.
Najczęściej zadawane pytania
Czym jest dekorator w Pythonie?
Dekorator to funkcja, która przyjmuje inną funkcję i zwraca nową funkcję, zwykle taką, która opakowuje oryginał dodatkowym zachowaniem. Stosujesz go przez @decorator_name w linii nad def. Składnia @ to skrót od func = decorator_name(func).
Do czego służą dekoratory w Pythonie?
Do dodawania zachowania wokół funkcji bez edytowania jej ciała: logowania, pomiaru czasu, cache'owania, sprawdzania uwierzytelnienia, walidacji wejścia, ponawiania. Frameworki używają ich intensywnie: @app.route(...) we Flasku, @pytest.fixture w pytest, a wbudowane są @property i @staticmethod.
Czy mogę napisać własny dekorator?
Tak. Dekorator to po prostu funkcja, która przyjmuje funkcję i zwraca funkcję. Większość własnych dekoratorów opakowuje oryginalne wywołanie w małą funkcję wewnętrzną, która robi coś przed nim, po nim albo wokół niego. Użyj functools.wraps na funkcji wewnętrznej, żeby zachować oryginalną nazwę i docstring.