Dekoratory

Funkcje, które opakowują inne funkcje i dodają im zachowanie, np. logowanie lub sprawdzanie uprawnień. Zapis @nazwa.

Na tej stronie

Przykład

#
Python
import functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Wywołuję {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} zwróciła {result!r}")
        return result
    return wrapper

@log_call
def award(name, exp):
    """Przyznaje punkty doświadczenia."""
    return f"{name}: +{exp} EXP"

award("Ania", 15)
print(award.__name__, "|", award.__doc__)
Wynikzapisany wynik, możesz go sprawdzić
Wywołuję award('Ania', 15)
award zwróciła 'Ania: +15 EXP'
award | Przyznaje punkty doświadczenia.

Definicja i zastosowanie

#

Dekorator to funkcja, która przyjmuje inną funkcję i zwraca jej rozszerzoną wersję. Zapis @log_call nad definicją funkcji to skrót od award = log_call(award). Mechanizm działa, bo w Pythonie funkcje są zwykłymi obiektami: można je przekazywać jako argumenty i zwracać z innych funkcji.

Typowy dekorator definiuje w środku funkcję opakowującą (wrapper). Przyjmuje ona dowolne argumenty przez *args i **kwargs, robi coś przed wywołaniem oryginału, wywołuje go, robi coś po i zwraca jego wynik. W ten sposób dodasz logowanie, pomiar czasu, sprawdzanie uprawnień czy ponawianie prób bez zmieniania samych funkcji.

Wrapper zasłania nazwę i docstring oryginalnej funkcji: bez dodatkowego kroku award.__name__ zwróciłoby "wrapper". Dekorator @functools.wraps(func) nałożony na wrapper kopiuje te informacje, dlatego warto go używać zawsze. Dekorator z własnymi parametrami, np. @require_level(5), wymaga jeszcze jednego poziomu: zewnętrzna funkcja przyjmuje parametry i zwraca właściwy dekorator.

Kilka dekoratorów nad jedną funkcją działa od dołu: najbliższy definicji opakowuje ją jako pierwszy. Biblioteka standardowa ma wiele gotowych dekoratorów, np. @property, @classmethod, @dataclass czy @functools.lru_cache.

Składnia

#
Składnia
import functools

def dekorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wynik = func(*args, **kwargs)
        return wynik
    return wrapper

@dekorator
def funkcja():
    ...

Więcej przykładów

#
Dekorator z parametrem
Python
import functools

def require_level(minimum):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(player, *args, **kwargs):
            if player["level"] < minimum:
                return f"{player['name']}: potrzebny poziom {minimum}"
            return func(player, *args, **kwargs)
        return wrapper
    return decorator

@require_level(5)
def enter_world(player, world):
    return f"{player['name']} wchodzi do świata {world}"

print(enter_world({"name": "Ania", "level": 7}, 3))
print(enter_world({"name": "Kuba", "level": 2}, 3))
Wynikzapisany wynik, możesz go sprawdzić
Ania wchodzi do świata 3
Kuba: potrzebny poziom 5
Kolejność kilku dekoratorów
Python
def bold(func):
    def wrapper():
        return f"**{func()}**"
    return wrapper

def exclaim(func):
    def wrapper():
        return f"{func()}!"
    return wrapper

@bold
@exclaim
def title():
    return "Nowy poziom"

print(title())
Wynikzapisany wynik, możesz go sprawdzić
**Nowy poziom!**

Zapis jest równoważny title = bold(exclaim(title)), więc wykrzyknik dodaje się przed pogrubieniem.

Dekorator, który zlicza wywołania
Python
import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def check_answer(answer):
    return answer == "B"

for answer in ["A", "B", "C"]:
    check_answer(answer)

print(check_answer.calls)
Wynikzapisany wynik, możesz go sprawdzić
3

Dobre praktyki

#
  • Zawsze dodawaj @functools.wraps(func) do funkcji opakowującej. Zachowasz nazwę i docstring oryginału, co ułatwia debugowanie i czytanie komunikatów błędów.
  • Wrapper powinien przyjmować *args, **kwargs i zwracać wynik oryginału. Bez return każda udekorowana funkcja zacznie zwracać None.
  • Kod samego dekoratora uruchamia się raz, przy definiowaniu funkcji, a wrapper przy każdym wywołaniu. To, co ma działać przy każdym wywołaniu, umieść wewnątrz wrappera.

Powiązane hasła

#

Widzisz błąd albo brakuje przykładu? Napisz do nas.