Maszyny stanów skończonych w Pythonie

PL
Data dodania: 2011-09-17, Autor: imbaczek, Dodał: Karol, Wyświetleń: 370

Wprowadzenie

Maszyny stanów skończonych (ang. Finite State Machine) to abstrakcyjne, matematyczne opisy zachowań systemów dynamicznych. Brzmi groźnie, prawda?

Okazuje się, że nie jest to takie straszne w praktyce. FSM są podwaliną np. wyrażeń regularnych w Perlu (i innych językach), odpowiadają za nawiązywanie połączeń TCP, znajdują się w grach komputerowych; w zasadzie są wszędzie. (Gdy dodać do nich nieskończoną pamięć, dostajemy maszynę Turinga, czyli prawie zwykły komputer.)

Zacznijmy od przykładu: spróbujmy znaleźć jakieś słowo w zdaniu. Powiedzmy, że szukamy słowa "kot", a zdanie to "Ala ma kota." W Pythonie ten problem łatwo rozwiązać bez pomocy matematyki dyskretnej, w taki sposób:

>>> "kot" in "Ala ma kota."
True
>>>

Niestety, z punktu widzenia tematu artykułu, wartość dydaktyczna tego eleganckiego rozwiązania jest żadna. Zastanówmy się, co robi ta prosta linijka kodu:

"kot" in "Ala ma kota."

Pierwszym pomysłem mógłby być taki algorytm:

  1. Zacznij od pierwszej pozycji w zdaniu.
  2. Jeśli na obecnej pozycji jest słowo "kot", to zakończ i zwróć True.
  3. Przejdź na następną pozycję, zwróć False jeśli koniec zdania; w przeciwnym przypadku przejdź do kroku 2.

Takie podejście, choć działające, jest nieoptymalne - każdy znak w zdaniu porównujemy niepotrzebnie kilka razy. Unikniemy tego tworząc automat stanów.

Automat stanów szukający słowa "kot".

Implementacja

Automaty można implementować na kilka sposobów; zacznijmy od najprostszego, czyli serii porównań.

def automat1(lancuch):
    stan = 'start'
    for znak in lancuch:
        if stan == 'start':
            if znak == 'k':
                stan = 'k'
        elif stan == 'k':
            if znak == 'o':
                stan = 'ko'
            else:
                stan = 'start'
        elif stan == 'ko':
            if znak == 't':
                return True
            elif znak == 'k':
                stan = 'k'
            else:
                stan = 'start'
    return False

Efekt działania (z drobną modyfikacją wypisującą stan i znak):

>>> automat1("Ala ma kota.")
A start
l start
a start
  start
m start
a start
  start
k start
o k
t ko
True
>>>

Nie jest to najszczęśliwsza implementacja - nie jest zbyt wydajna, a czytelność pozostawia wiele do życzenia. Stworzenie maszyny z większą ilością stanów w taki sposób ręcznie mija się z celem.

Drugi sposób to wykorzystanie struktur danych udostępnianych przez język. Użyjemy słownika (dicta w slangu Pythona) do przechowania tablicy przejść stanów.

tablica = {
    'start': {
        'k': 'k',
        None: 'start' },
    'k': {
        'o': 'ko',
        None: 'start' },
    'ko': {
        't': 'kot',
        'k': 'k',
        None: 'start' },
    'kot': {
        None: 'kot' }
}

Jak widać, użyliśmy None w znaczeniu "znak inny niż zdefiniowane." A oto gotowa funkcja realizująca nasz automat:

def automat2(lancuch):
    tablica = {
        'start': {
            'k': 'k',
            None: 'start' },
        'k': {
            'o': 'ko',
            None: 'start' },
        'ko': {
            't': 'kot',
            'k': 'k',
            None: 'start' },
        'kot': {
            None: 'kot' },
    }
    stan = 'start'
    for znak in lancuch:
        if znak in tablica[stan]:
            stan = tablica[stan][znak]
        else:
            stan = tablica[stan][None]
        if stan == 'kot':
            return True
    return False

Efekt (ponownie z modyfikacją):

>>> automat2("Ala ma kota.")
A start
l start
a start
  start
m start
a start
  start
k start
o k
t ko
True
>>>

Bez zmian, czyli działa.

Przejście na styl deklaratywny (czyli opis maszyny za pomocą danych, a nie kodu) zdecydowanie upraszcza zapis i poprawia czytelność. Do tego można łatwo zgeneralizować naszą funkcję:

def automat2_uogolniony(lancuch, tablica, start, koniec):
    stan = start
    for znak in lancuch:
        if znak in tablica[stan]:
            stan = tablica[stan][znak]
        else:
            stan = tablica[stan][None]
        if stan == koniec:
            return True
    return False

Dzięki temu wystarczy napisać odpowiednią tabelę przejść stanów, a funkcja załatwi resztę. Warto zauważyć, że dzięki dynamicznemu charakterowi Pythona ta funkcja nie jest ograniczona do łańcuchów znaków - może to być równie dobrze lista dowolnych obiektów.

Tablica przejść stanów jest dobrym i dość ogólnym sposobem, ale co, jeśli potrzebujemy wykonać jakąś akcję związaną ze stanami lub przejściami stanów? Najładniej byłoby zaprząc do tego zadania klasy z odpowiednimi metodami. A skoro będziemy mieli klasy, to można znowu wykorzystać dynamiczność Pythona w intrygujący sposób.

Mianowicie, w Pythonie można, podobnie jak w Javie i C#, w czasie wykonania poznać informacje o typach obiektów - ale nie tylko. Można także typ obiektu zmienić. Wygląda to tak:

>>> class A(object):
...   def m(self): print 'jestem A.m'
...
>>> class B(object):
...   def f(self): print 'a ja jestem B.f'
...
>>> o = A()
>>> type(o)
<class '__main__.A'>
>>> o.m()
jestem A.m
>>> o.__class__ = B
>>> type(o)
<class '__main__.B'>
>>> o.f()
a ja jestem B.f

Po tej operacji metoda m() nie jest już dostępna:

>>> o.m()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'B' object has no attribute 'm'
>>>

Oczywiście, gdyby w klasie B zamiast metody "f" utworzyć metodę "m", to o.m() zadziałałoby po podmianie typu. Wykorzystamy właśnie tę własność.

class Stan(object):
    def przejdz_do(self, stan):
        self.przed_przejsciem(stan)
        klasa = self.__class__
        self.__class__ = stan
        self.po_przejsciu(klasa)

    def przed_przejsciem(self, nowy_stan):
        """Tu mozna zdefiniowac operacje do wykonania przed przejsciem w nowy stan."""
        pass

    def po_przejsciu(self, stary_stan):
        """Tu mozna zdefiniowac operacje do wykonania po przejsciu w nowy stan ze starego."""
        pass

    def przejscie(self, wejscie):
        raise NotImplementedError("przejscie niezdefiniowane dla stanu %s" % self.__class__)

    def akceptacja(self):
        """Podmnien te metode, aby zwracala True, jesli mozna zakonczyc automat."""
        return False

Teraz zdefiniujemy stany i przejścia:

class Start(Stan):
    def przejscie(self, wejscie):
        if wejscie == 'k':
            self.przejdz_do(K)
    

class K(Stan):
    def przejscie(self, wejscie):
        if wejscie == 'o':
            self.przejdz_do(KO)
        else:
            self.przejdz_do(Start)


class KO(Stan):
    def przejscie(self, wejscie):
        if wejscie == 't':
            self.przejdz_do(KOT)
        elif wejscie == 'k':
            self.przejdz_do(K)
        else:
            self.przejdz_do(Start)


class KOT(Stan):
    def przejscie(self, wejscie):
        pass

    def akceptacja(self):
        return True

I na koniec funkcja wykonująca automat na łańcuchu danych:

def automat3(lancuch):
    stan = Start()
    for znak in lancuch:
        stan.przejscie(znak)
        if stan.akceptacja():
            return True
    return False

Efekt (tym razem modyfikcja polega na wypisaniu znaku i nazwy klasy):

>>> automat3("Ala ma kota.")
A Start
l Start
a Start
  Start
m Start
a Start
  Start
k Start
o K
t KO
True
>>>

Jako ćwiczenie dla czytelnika pozostawię połączenie podejścia drugiego z trzecim (tzn. połączenia dynamicznego zmieniania typu z tablicą przejść stanów).

Podejście z obiektami zmieniającymi typ jest tym lepsze, im bardziej skomplikowana jest implementowana maszyna stanów i środowisko, w którym przyjdzie jej pracować. Przykład z szukaniem wyrazu nie oddaje w pełni możliwości, jakie daje ta strategia - można sobie wyobrazić np. prostą sztuczną inteligencję do jakiejś gry, która oprócz zmieniania stanu potrzebuje wykonywać dodatkowe operacje i przechowywać informacje nie związane bezpośrednio z automatem skończonym.

 


Aby dodawać komentarze musisz być zalogowany!


Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?