W tym wpisie chciałbym przedstawić konstrukcje do zarządzania stanem jakie oferuje język Python.
Funkcja
Na początek warto poruszyć szczególny przypadek, jakim jest funkcja. Z założenia funkcja nie ma stanu więc nie powinna być rozpatrywana pod kątem jego zarządzania. Należy jednak podkreślić, że istnieje wiele odstępstw od tej reguły, o ile funkcja opiera swoje działanie:
- na korzystaniu z globalnych lub
- posiada mutowalny domyślny argument lub
- powoduje efekty uboczne, np. zapisanie danych do pliku.
W takich przypadkach możemy powiedzieć, że funkcja rzeczywiście zarządza stanem.
Domknięcie
Domknięcie to funkcja, która pamięta zmienne z zakresu funkcji nadrzędnej. Na pierwszy rzut oka wydaje się ona trudniejszym pojęciem do przyswojenia, ale w rzeczywistości tak nie jest.
W gruncie rzeczy chodzi tylko o hermetyzację stanu, czyli o utworzenie stanu w funkcji nadrzędnej, aby tylko funkcja wewnętrzna mogła nim zarządzać. To ograniczenie powoduje, że zmiana stanu jest możliwa jedynie w obrębie stworzonej funkcji.
W poniższym przykładzie dochodzi do zwrócenia nowej funkcji zarówno dla c1 jak i c2. Każda z tych funkcji pamięta swój własny licznik.
def make_counter():
i = 0
def counter():
nonlocal i
result = i
i += 1
return result
return counter
c1 = make_counter()
print(c1()) # 0
print(c1()) # 1
print(c1()) # 2
c2 = make_counter()
print(c2()) # 0Jeśli przykład z licznikami wydaje Ci się zbyt uproszczony, to zachęcam Cię do analizy praktycznego przypadku z biblioteki Selenium.
Generator
Kolejną konstrukcją dostępną w Pythonie, która świetnie sprawdza się w zarządzaniu stanem jest generator. Jest to rozwiązanie o charakterze pośrednim między domknięciem, a klasą. Temat generatora został omówiony w osobnym wpisie.
Przykład z użyciem generatora przedstawia analogiczny przypadek z odliczaniem:
def make_counter():
i = 0
while True:
yield i
i += 1
c1 = make_counter()
print(next(c1)) # 0
print(next(c1)) # 1
print(next(c1)) # 2
c2 = make_counter()
print(next(c2)) # 0
Największa różnica jaka jest pomiędzy użyciem generatora, a domknięcia to ilość zapisanego kodu. W przypadku generatora jest go zdecydowanie mniej, co więcej ta konstrukcja jest łatwiejsza niż funkcje wyższego rzędu. Warto wspomnieć również, że użycie generatora częściej będzie dotyczyć operacji związanych z iteracją.
Klasa
Następną konstrukcją, której nie mogło tu zabraknąć jest klasa. Poniższy przykład kodu również przedstawia odliczanie.
class Counter:
def __init__(self):
self._i = 0
def next(self):
result = self._i
self._i += 1
return result
c1 = Counter()
print(c1.next()) # 0
print(c1.next()) # 1
print(c1.next()) # 2
c2 = Counter()
print(c2.next()) # 0Klasy, w przeciwieństwie do domknięcia czy generatora, charakteryzuje możliwość manipulowania stanem z poziomu wielu różnych metod. To sprawia, że klasy są dużo bardziej elastyczne w zakresie zarządzania stanem.
Specjalnym przypadkiem klasy, który zasługuje na uwagę jest funktor, czyli klasa, która implementuje magiczną metodę call. Obiekty tej klasy można wywoływać podobnie jak zwykłe funkcje.
class Counter:
def __init__(self):
self._i = 0
def __call__(self, *args, **kwargs):
result = self._i
self._i += 1
return result
c1 = Counter()
print(c1()) # 0
print(c1()) # 1
print(c1()) # 2
c2 = Counter()
print(c2()) # 0
Podsumowanie
Jedną ze szczególnych zalet Pythona, którą bardzo cenię, to możliwość korzystania z wielu różnych konstrukcji. Oczywiście większość, jak nie wszystkie przypadki związane ze stanem, można rozwiązać z użyciem klas. To co jednak przemawia za alternatywami to fakt, że są prostsze i krótsze, a co za tym idzie łatwiejsze w zrozumieniu.