Что такое Pythonic для реализации простого FSM?

Вчера мне пришлось разбирать очень простой файл двоичных данных - это правило, ищите два байта в строке, которые являются как 0xAA, тогда следующий байт будет байтом длины, затем пропускает 9 байтов и выводит заданную сумму данных оттуда. Повторите в конце файла.

Мое решение действительно сработало и очень быстро собралось (хотя я программист C в глубине души, я все еще думаю, что мне было быстрее написать это на Python, чем в C) - НО, он явно не является Pythonic, и он читается как программа на C (и не очень хороший в этом!)

Какой был бы лучший/более питонический подход к этому? Является ли простой FSM подобным, хотя это еще правильный выбор в Python?

Мое решение:

#! /usr/bin/python

import sys

f = open(sys.argv[1], "rb")

state = 0

if f:
    for byte in f.read():
        a = ord(byte)       
        if state == 0:
            if a == 0xAA:
                state = 1
        elif state == 1:
            if a  == 0xAA:
                state = 2
            else: 
                state = 0
        elif state == 2:
            count = a;
            skip = 9
            state = 3
        elif state == 3:
            skip = skip -1
            if skip == 0:
                state = 4
        elif state == 4:
             print "%02x" %a
             count = count -1 
             if count == 0:
                 state = 0
                 print "\r\n"

Ответ 1

Вы можете указать свои постоянные имена состояний вместо использования 0, 1, 2 и т.д. для улучшения удобочитаемости.

Вы можете использовать словарь для отображения (current_state, input) -> (next_state), но на самом деле это не позволяет вам выполнять дополнительную обработку во время переходов. Если вы не включите некоторую "функцию перехода", чтобы выполнить дополнительную обработку.

Или вы можете сделать подход, не относящийся к FSM. Я думаю, что это будет работать до тех пор, пока 0xAA 0xAA появляется только тогда, когда указывает "начало" (не отображается в данных).

with open(sys.argv[1], 'rb') as f:
    contents = f.read()
    for chunk in contents.split('\xaa\xaa')[1:]:
        length = ord(chunk[0])
        data = chunk[10:10+length]
        print data

Если он появляется в данных, вы можете вместо этого использовать string.find('\xaa\xaa', start) для сканирования по строке, установив аргумент start, чтобы начать искать, где закончился последний блок данных. Повторяйте, пока не вернется -1.

Ответ 2

Самый классный способ, который я видел для реализации FSM в Python, должен выполняться через генераторы и сопрограммы. См. Этот Очаровательный пост Python для примера. Эли Бендерски также отличное отношение к теме.

Если coroutines не знакомы с территорией, Дэвид Безли Любопытный курс по Corouts и Concurrency - это звездное введение.

Ответ 3

Я немного опасаюсь рассказывать кому-нибудь, что такое Pythonic, но здесь. Во-первых, имейте в виду, что в функциях python есть только объекты. Переходы могут быть определены с помощью словаря, который имеет значение (input, current_state) в качестве ключа и кортеж (next_state, action) в качестве значения. Действие - это просто функция, которая делает все необходимое для перехода от текущего состояния к следующему состоянию.

Вот хороший пример этого в http://code.activestate.com/recipes/146262-finite-state-machine-fsm. Я не использовал его, но из быстрого чтения кажется, что он охватывает все.

Аналогичный вопрос был задан/ответил здесь пару месяцев назад: Python state-machine design. Возможно, вы найдете также полезные ответы.

Ответ 4

Я думаю, ваше решение выглядит отлично, за исключением того, что вы должны заменить count = count - 1 на count -= 1.

Это один из тех моментов, когда призрачные кодовые показы будут вызывать способы определения состояний сопоставления диктофона для вызывающих, с небольшой функцией драйвера, но это не лучше, просто причудливо и использование более неясных языковых функций.

Ответ 6

Я думаю, что самый питонический способ будет, как и то, что предлагал FogleBird, но отображение из (текущего состояния, ввода) функции, которая обрабатывала бы обработку и переход.

Ответ 7

Вы можете использовать регулярные выражения. Что-то вроде этого кода найдет первый блок данных. Тогда это просто случай начала следующего поиска после предыдущего совпадения.

find_header = re.compile('\xaa\xaa(.).{9}', re.DOTALL)
m = find_header.search(input_text)
if m:
    length = chr(find_header.group(1))
    data = input_text[m.end():m.end() + length]