Dekoratory stanowią potężne narzędzie w języku Python. Dzięki nim możemy w elegancki sposób modyfikować działanie istniejących funkcji, „opakowując” je w dodatkowe funkcje. To pozwala na tworzenie bardziej klarownego i zwięzłego kodu, a także na łatwe współdzielenie funkcjonalności między różnymi częściami programu. Ten artykuł nie tylko wyjaśni, jak używać dekoratorów, ale także poprowadzi Cię przez proces ich tworzenia.
Wymagana wiedza
Zrozumienie koncepcji dekoratorów w Pythonie wymaga pewnego przygotowania. Poniżej znajdziesz listę zagadnień, które warto znać, aby w pełni skorzystać z tego poradnika. Jeśli któreś z nich okażą się niejasne, dołączone linki pomogą Ci odświeżyć potrzebne informacje.
Podstawy Pythona
Ten temat jest przeznaczony dla osób z pewnym doświadczeniem w Pythonie. Zanim przejdziesz dalej, powinieneś dobrze znać podstawowe konstrukcje języka, takie jak typy danych, funkcje, obiekty i klasy. Powinieneś również rozumieć podstawowe pojęcia programowania obiektowego, jak metody dostępowe (gettery), modyfikujące (settery) oraz konstruktory. Jeśli jesteś nowy w Pythonie, zachęcamy do zapoznania się z materiałami, które wprowadzą Cię w ten język.
Funkcje jako obiekty pierwszej klasy
Oprócz podstaw, ważna jest znajomość koncepcji „funkcji jako obiektów pierwszej klasy”. W Pythonie funkcje, podobnie jak liczby czy ciągi znaków, są obiektami. To oznacza, że można z nimi wykonywać następujące operacje:
- Przekazywać funkcję jako argument do innej funkcji, analogicznie do przekazywania liczb lub tekstów.
- Zwracać funkcję jako wynik działania innej funkcji, tak jak zwraca się inne typy danych.
- Przechowywać funkcje w zmiennych.
Różnica między funkcjami a innymi obiektami polega głównie na tym, że funkcje posiadają specjalną metodę `__call__()`, która umożliwia ich wywoływanie.
Mając tę wiedzę, możemy teraz przejść do sedna tematu i omówić czym są dekoratory.
Czym jest dekorator Pythona?
Dekorator w Pythonie to nic innego jak funkcja, która przyjmuje inną funkcję jako argument i zwraca jej zmodyfikowaną wersję. Innymi słowy, funkcja `dekorator` jest dekoratorem, jeśli przyjmuje funkcję `bazowa` jako argument i zwraca nową funkcję `zmodyfikowana`. Funkcja `zmodyfikowana` zawiera w sobie wywołanie `bazowej` funkcji, ale może również wykonywać dodatkowe operacje przed i po wywołaniu `bazowej`.
Aby to lepiej zilustrować, oto przykład kodu:
# Funkcja 'dekorator' jest dekoratorem, przyjmuje inną funkcję 'bazowa' jako argument def dekorator(bazowa): # Tworzymy funkcję 'zmodyfikowana', która jest zmodyfikowaną wersją 'bazowej' # 'zmodyfikowana' wywoła funkcję 'bazowa', ale może robić coś przed i po wywołaniu def zmodyfikowana(): # Przed wywołaniem 'bazowej' wypisujemy coś na ekranie print("Początek działania") # Następnie wywołujemy funkcję 'bazowa' bazowa() # Po wywołaniu 'bazowej' wypisujemy coś innego na ekranie print("Koniec działania") # Ostatecznie funkcja 'dekorator' zwraca 'zmodyfikowana', zmodyfikowaną wersję 'bazowej' return zmodyfikowana
Jak utworzyć dekorator w Pythonie?
Aby lepiej zrozumieć proces tworzenia i używania dekoratorów, przejdziemy przez prosty przykład. Stworzymy dekorator logujący, który będzie rejestrował nazwę dekorowanej funkcji za każdym razem, gdy ta funkcja zostanie wywołana.
Zaczynamy od definicji funkcji dekoratora, która przyjmuje argument `func`, czyli funkcję, którą chcemy udekorować:
def utworz_logger(func): # Tutaj będzie ciało funkcji
Wewnątrz dekoratora zdefiniujemy funkcję pomocniczą, która przed wywołaniem `func` wypisze jej nazwę:
# Wewnątrz funkcji utworz_logger def zmodyfikowana_funkcja(): print("Wywołuję: ", func.__name__) func()
Funkcja `utworz_logger` na koniec zwraca `zmodyfikowana_funkcja`, dzięki czemu ma ona możliwość wykonywania dodatkowych akcji. Kompletna definicja dekoratora wygląda następująco:
def utworz_logger(func): def zmodyfikowana_funkcja(): print("Wywołuję: ", func.__name__) func() return zmodyfikowana_funkcja
Funkcja `utworz_logger` jest naszym dekoratorem. Przyjmuje ona funkcję jako argument i zwraca nową funkcję, która przed wykonaniem oryginalnej funkcji, loguje jej nazwę.
Jak używać dekoratorów w Pythonie?
Aby zastosować dekorator do funkcji, używamy składni z symbolem `@`, w następujący sposób:
@utworz_logger def powiedz_hello(): print("Witaj, świecie!")
Teraz, gdy wywołamy funkcję `powiedz_hello()`, na ekranie zobaczymy:
Wywołuję: powiedz_hello Witaj, świecie!
Co tak naprawdę robi `@utworz_logger`? Stosuje dekorator do funkcji `powiedz_hello`. Kod poniżej da ten sam rezultat, co umieszczenie `@utworz_logger` nad definicją `powiedz_hello`.
def powiedz_hello(): print("Witaj, świecie!") powiedz_hello = utworz_logger(powiedz_hello)
Dekoratory w Pythonie można stosować na dwa sposoby. Pierwszy to jawne wywołanie dekoratora z funkcją jako argumentem, jak w powyższym kodzie. Drugi, bardziej zwięzły sposób, to użycie składni z symbolem `@`.
W tej części omówiliśmy, jak tworzyć i używać dekoratorów w Pythonie.
Bardziej skomplikowane przykłady
Powyższy przykład był dość prosty. Istnieją bardziej złożone sytuacje, takie jak dekorowanie funkcji, które przyjmują argumenty, lub dekorowanie całej klasy. Przeanalizujemy te przypadki.
Gdy funkcja przyjmuje argumenty
Jeśli funkcja, którą dekorujemy, przyjmuje argumenty, nasza zmodyfikowana funkcja również musi je przyjąć i przekazać do oryginalnej funkcji. Wróćmy do naszego schematu z `dekorator` – `bazowa` – `zmodyfikowana`. Jeżeli `bazowa` przyjmuje argumenty, to `zmodyfikowana` musi je odebrać i przekazać podczas wywołania `bazowej`. Oto jak to wygląda w kodzie:
def dekorator(bazowa): def zmodyfikowana(*args, **kwargs): # Tutaj możemy zrobić coś przed wywołaniem bazowej ___ # Następnie wywołujemy bazową przekazując argumenty *args i **kwargs bazowa(*args, **kwargs) # Tutaj możemy zrobić coś po wywołaniu bazowej ___ return zmodyfikowana
Gwiazdki `*args` i `**kwargs` oznaczają odpowiednio argumenty pozycyjne i nazwane (słowa kluczowe).
`zmodyfikowana` ma dostęp do argumentów, co umożliwia sprawdzanie ich poprawności przed wywołaniem `bazowej`.
Przykładem może być dekorator `sprawdz_string`, który upewnia się, że argument przekazany do dekorowanej funkcji jest ciągiem znaków:
def sprawdz_string(func): def udekorowana_func(text): if not isinstance(text, str): raise TypeError('Argument funkcji ' + func.__name__ + ' musi być ciągiem znaków.') else: func(text) return udekorowana_func
Możemy użyć tego dekoratora dla funkcji `powiedz_hello`:
@sprawdz_string def powiedz_hello(imie): print('Witaj', imie)
A następnie przetestować kod:
powiedz_hello('Jan') # Działa poprawnie powiedz_hello(3) # Powinien wygenerować wyjątek
Wynik działania to:
Witaj Jan Traceback (most recent call last): File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> powiedz_hello(3) # Powinien wygenerować wyjątek File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in udekorowana_func raise TypeError('Argument funkcji + func._name_ + musi być ciągiem znaków.') TypeError: Argument funkcji powiedz_hello musi być ciągiem znaków. $0
Jak widać, wywołanie `powiedz_hello(’Jan’)` zakończyło się sukcesem, a próba wywołania z liczbą 3 wygenerowała wyjątek. Dekorator `sprawdz_string` może być użyty do sprawdzania poprawności argumentów każdej funkcji, która wymaga ciągu znaków.
Dekorowanie klasy
Dekoratory mogą być również używane do dekorowania klas. W takim przypadku dekorowana metoda zastępuje konstruktor klasy (`__init__`). W naszym schemacie, jeśli `dekorator` jest dekoratorem, a `Klasa` jest klasą, którą dekorujemy, to `dekorator` ozdobi metodę `Klasa.__init__`. Jest to użyteczne, gdy chcemy wykonać pewne akcje przed utworzeniem instancji obiektu danej klasy.
Oznacza to, że poniższy kod:
def dekorator(func): def nowa_func(*args, **kwargs): print('Coś robimy przed utworzeniem instancji') func(*args, **kwargs) return nowa_func @dekorator class Klasa: def __init__(self): print("W konstruktorze")
jest równoznaczny z kodem:
def dekorator(func): def nowa_func(*args, **kwargs): print('Coś robimy przed utworzeniem instancji') func(*args, **kwargs) return nowa_func class Klasa: def __init__(self): print("W konstruktorze") Klasa.__init__ = dekorator(Klasa.__init__)
Utworzenie instancji klasy `Klasa` za pomocą jednej z tych dwóch metod da ten sam wynik:
Coś robimy przed utworzeniem instancji W konstruktorze
Przykładowe dekoratory w Pythonie
Chociaż możesz tworzyć własne dekoratory, Python ma już kilka wbudowanych. Poniżej przedstawiamy kilka z nich, z którymi możesz się spotkać:
@staticmethod
Dekorator `@staticmethod` oznacza, że dana metoda w klasie jest statyczna. Metody statyczne nie wymagają instancji klasy, aby mogły być wywołane. Poniższy przykład przedstawia klasę `Pies` z metodą statyczną `szczekaj`:
class Pies: @staticmethod def szczekaj(): print('Hau, hau!')
Dostęp do metody `szczekaj` uzyskujemy w następujący sposób:
Pies.szczekaj()
Wykonanie kodu da następujący wynik:
Hau, hau!
Jak wspomnieliśmy, dekoratory można używać na dwa sposoby. Składnia z `@` jest bardziej zwięzła i czytelna. Inną metodą jest wywołanie dekoratora, przekazując funkcję jako argument. Oznacza to, że powyższy kod jest równoważny z poniższym:
class Pies: def szczekaj(): print('Hau, hau!') Pies.szczekaj = staticmethod(Pies.szczekaj)
I nadal możemy używać metody `szczekaj` w ten sam sposób:
Pies.szczekaj()
I otrzymamy ten sam rezultat:
Hau, hau!
Pierwsza metoda jest bardziej czytelna i intuicyjna. W pozostałych przykładach będziemy używać pierwszej metody.
@classmethod
Ten dekorator służy do oznaczania metody jako metody klasowej. Metody klasowe, podobnie jak statyczne, nie wymagają instancji klasy.
Główna różnica polega na tym, że metody klasowe mają dostęp do atrybutów klasy, co nie jest możliwe w przypadku metod statycznych. Python automatycznie przekazuje klasę jako pierwszy argument do metody klasowej. Aby utworzyć metodę klasową, możemy użyć dekoratora `@classmethod`:
class Pies: @classmethod def kim_jestem(cls): print("Jestem " + cls.__name__ + "!")
Aby wywołać metodę, używamy zapisu:
Pies.kim_jestem()
A wynik to:
Jestem Pies!
@property
Dekorator `@property` służy do oznaczania metody jako „getter” dla właściwości. Wróćmy do przykładu z psem i stwórzmy metodę, która będzie pobierała imię psa:
class Pies: # Tworzymy konstruktor, który przyjmuje imię psa def __init__(self, imie): # Tworzymy prywatną właściwość imie # Podwójne podkreślniki sprawiają, że atrybut jest prywatny self.__imie = imie @property def imie(self): return self.__imie
Teraz możemy uzyskać dostęp do imienia psa jak do zwykłej właściwości:
# Tworzymy instancję klasy pies = Pies('Burek') # Dostęp do właściwości imie print("Imię psa to:", pies.imie)
A wynik to:
Imię psa to: Burek
@property.setter
Dekorator `@property.setter` służy do tworzenia metody ustawiającej dla właściwości. Aby użyć tego dekoratora, zamieniasz `property` na nazwę właściwości, dla której tworzysz setter. Na przykład, jeśli tworzysz setter dla właściwości `imie`, to twój dekorator będzie wyglądał `@imie.setter`. Ilustrujemy to przykładem z psem:
class Pies: # Konstruktor przyjmujący imię psa def __init__(self, imie): # Prywatna właściwość imie self.__imie = imie @property def imie(self): return self.__imie # Tworzymy setter dla właściwości imie @imie.setter def imie(self, nowe_imie): self.__imie = nowe_imie
Aby przetestować setter, użyjemy kodu:
# Tworzymy nowego psa pies = Pies('Burek') # Zmieniamy imię psa pies.imie="Azor" # Wypisujemy nowe imię psa print("Nowe imię psa to:", pies.imie)
Uruchomienie kodu da następujący wynik:
Nowe imię psa to: Azor
Znaczenie dekoratorów w Pythonie
Po omówieniu, czym są dekoratory i przeanalizowaniu kilku przykładów, możemy przyjrzeć się, dlaczego są one ważne w Pythonie. Dekoratory są istotne z kilku powodów, a niektóre z nich wymieniono poniżej:
- Umożliwiają ponowne wykorzystanie kodu: w przykładzie z logowaniem możemy użyć `@utworz_logger` dla dowolnej funkcji. To pozwala dodawać logowanie do wielu funkcji bez pisania tego samego kodu wielokrotnie.
- Pozwalają pisać bardziej modułowy kod: w przykładzie z logowaniem, dzięki dekoratorom, możesz oddzielić główną funkcjonalność (`powiedz_hello`) od dodatkowej (logowanie).
- Usprawniają tworzenie frameworków i bibliotek: dekoratory są powszechnie stosowane w frameworkach i bibliotekach Pythona, aby dodawać dodatkową funkcjonalność. Na przykład w frameworkach webowych takich jak Flask czy Django, dekoratory służą do definiowania tras, obsługi uwierzytelniania czy stosowania oprogramowania pośredniego.
Podsumowanie
Dekoratory są bardzo przydatne. Możesz ich używać do rozszerzania funkcjonalności bez modyfikowania ich podstawowej logiki. Jest to przydatne, gdy chcesz mierzyć czas wykonywania funkcji, rejestrować każde jej wywołanie, sprawdzać poprawność argumentów przed wywołaniem lub weryfikować uprawnienia przed uruchomieniem funkcji. Po zrozumieniu dekoratorów będziesz w stanie pisać kod w bardziej przejrzysty i efektywny sposób.
Zachęcamy także do przeczytania naszych artykułów na temat krotek i korzystania z cURL w Pythonie.