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:

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()) # 0

Jeś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())  # 0

Klasy, 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.