Menu

Dekoratory w Pythonie: @funkcje, argumenty i functools.wraps

Czym naprawdę są dekoratory w Pythonie, jak napisać własny i jakie wzorce (argumenty, nakładanie, wraps) sprawiają, że warto ich używać.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

  1. repeat(times=3) to wywołanie funkcji. Zwraca decorator.
  2. decorator to właściwy dekorator: przyjmuje funkcję i zwraca opakowaną.
  3. wrapper to 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:

  • @property zamienia metodę w obliczany atrybut.
  • @staticmethod oznacza metodę, która nie używa self ani cls.
  • @classmethod dostaje klasę jako cls zamiast instancji, co świetnie sprawdza się w alternatywnych konstruktorach.
  • @functools.lru_cache zapamię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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ