Я создаю приложение с GUI для сбора и обработки данных с микроконтроллера на Python с помощью PyQt. И вот я наконец-то доделал часть функционала, предназначенного для взаимодействия компьютера с платой STM32, теперь необходимо было сделать интерфейс для обработки данных, в котором легко можно было бы настраивать параметры выполнения программы. Я начал думать, как не вносить в программу кучу флагов с соответствующими if-else конструкциями, и вот, что я придумал.

Будь как Дрейк при написании кода
Будь как Дрейк при написании кода

Ещё немного контекста

Если кратко, то моя программа строит перемещение железнодорожной телеги с платой по данным гироскопа и акселерометра, этакая автономная система навигации. Отдельно собираются данные, затем запускается их обработка. В результате, пользователь получает необходимые графики и файлы с данными в удобном виде. Нюанс заключался в том, что я хотел предоставить пользователю возможность самому решать, какие результаты ему нужны, а какие нет. Например, в один раз пользователю необходимы и графики исходных данных, и графики отфильтрованных данных с помощью фильтра Калмана (теория алгоритма хорошо описана тут, а практическое применение тут). А в другой раз ему не нужны графики исходных данных. Для этого он активирует или деактивирует соответствующий checkBox в GUI. И вот как это реализовал.

Реализация

Для начала нужно состояния всех чекбоксов сохранить в один словарь любым способом. Получаем такой словарь:

processing_params = {
        'Raw_Data': {
            'plotting_init_data': True,
            'plotting_buffers_data': False,
            'plotting_raw_data': True,
            'kwargs': {}
        }
    }

Во вложенный словарь 'kwargs' можно записать необходимые параметры, которые будут принимать методы. В данном случае, оставим его пустым.

Всю последовательность шагов обработки данных я реализовал в отдельном классе DataProcessing, в котором содержатся как и методы обработки данных, так и необходимые поля, в которых хранятся необходимые данные.

Вот основные методы для визуализации собранных данных:

def _plotting_init_data(self):
    print('Построение графиков данных выставки из файла ...')
    ...

def _plotting_buffers_data(self):
    print('Построение графиков буферных данных из файла ...')
    ...

def _plotting_raw_data(self):
    print('Построение графиков исходных данных из файла ...')
    ...

А теперь вот моё ноухау (ну или очередной велосипед). В классе DataProsessing мы создаём второй словарь, ключи в котором полностью совпадают с ключами первого словаря, а в значения записываем сами методы, отвечающие за тот или иной функционал. Продолжая пример, создаём такой словарь:

self._config = {
    'Raw_Data': {
        'plotting_raw_data': self._plotting_raw_data,
        'plotting_init_data': self._plotting_init_data,
        'plotting_buffers_data': self._plotting_buffers_data
    }
}

И теперь мы можем запустить выполнение всех выбранных функций одним циклом!

# -------------------------------
# Визуализация исходных данных
# -------------------------------
def _raw_data_plotting(self):
    print('# -------------------------------')
    print('# Визуализация исходных данных')
    print('# -------------------------------')
    class_params: dir = self._config['Raw_Data']
    user_params: dir = self._parameters['Raw_Data']

    for key, value in user_params.items():
        if value:
            class_params[key](**user_params['kwargs'])

А благодаря тому, что Python позволяет передавать в функции неопределённое количество именованных переменных, функции могут иметь абсолютно разные входные параметры (что лучше избегать в данном контексте, но возможность такая есть).

В итоге, получаем только необходимые результаты выполнения и такой вывод в консоль:

# -------------------------------
# Визуализация исходных данных
# -------------------------------
Построение графиков данных выставки из файла ...
Построение данных из файла ...
Представленный код класса DataProsessing целиком:
class DataProcessing:
    def __init__(self, parameters: dict):
        self._received_data = {}
        self._parameters = parameters
        self._config = {
            'Raw_Data': {
                'plotting_raw_data': self._plotting_raw_data,
                'plotting_init_data': self._plotting_init_data,
                'plotting_buffers_data': self._plotting_buffers_data
            }
        }

    # ---------------------------------
    # Основная функция обработки данных
    # ---------------------------------
    def start(self):
        self._decoding()
        self._file_classification()
        self._raw_data_plotting()
        self._raw_data_analysis()

    # -------------------------------
    # Чтение данных из файлов
    # -------------------------------
    def _decoding(self):
        ...
        
    # -------------------------------
    # Классификация файлов
    # -------------------------------
    def _file_classification(self):
        ...

    # -------------------------------
    # Визуализация исходных данных
    # -------------------------------
    def _raw_data_plotting(self):
        print('# -------------------------------')
        print('# Визуализация исходных данных')
        print('# -------------------------------')
        class_params: dir = self._config['Raw_Data']
        user_params: dir = self._parameters['Raw_Data']

        for key, value in user_params.items():
            if value:
                class_params[key](**user_params['kwargs'])
                
    def _plotting_init_data(self):
        print('Построение графиков данных выставки из файла ...')
        ...
    
    def _plotting_buffers_data(self):
        print('Построение графиков буферных данных из файла ...')
        ...
    
    def _plotting_raw_data(self):
        print('Построение графиков исходных данных из файла ...')
        ...
  
if __name__ == "__main__":
    processing_params = {
            'Raw_Data': {
                'plotting_init_data': True,
                'plotting_buffers_data': False,
                'plotting_raw_data': True,
                'kwargs': {}
            }
        }
    data_processing = DataProcessing(processing_params)
    data_processing.start()

Заключение

Благодаря такой реализации, программу легко дополнять необходимым функционалом. Пользователю необходимо нажать буквально пару кнопок для детальной настройки результатов работы программы, а Вам, как разработчику, добавить несколько полей в нужные словари.

Возможно, есть более элегантный метод решения подобной задачи. Возможно, я сделал очередной велосипед. Но может быть, данная статья поможет Вам в своих проектах.

Всем добра.

Комментарии (23)


  1. Dhwtj
    05.08.2025 16:44

    Класс делает всё: читает, классифицирует, рисует, анализирует. Лучше разделить на:

    * DataLoader (читает и декодирует)

    * DataAnalyzer (анализирует)

    * Plotter (рисует)

    Хотя, я видел подобное на scala

    Паттерн интерпретатор или что-то такое

    // 1. Общий тип для всех операций
    trait DataTask
    
    // 2. Case-классы для конкретных задач (хранят данные для операции)
    case class PlotRawData(title: String) extends DataTask
    case class AnalyzeData(method: String) extends DataTask
    case class LoadDataFrom(path: String) extends DataTask
    
    // 3. Список задач (конфигурация)
    val pipeline: List[DataTask] = List(
      LoadDataFrom("/path/to/data"),
      PlotRawData(title = "Initial view"),
      AnalyzeData(method = "mean")
    )
    
    // 4. "Исполнитель", который разбирает и выполняет задачи
    def process(tasks: List[DataTask]): Unit = {
      tasks.foreach { task =>
        task match {
          case PlotRawData(title)   => println(s"Рисую график: $title")
          case AnalyzeData(method)  => println(s"Анализирую методом: $method")
          case LoadDataFrom(path)   => println(s"Загружаю данные из: $path")
        }
      }
    }
    
    process(pipeline)


    1. r3m4k Автор
      05.08.2025 16:44

      Из класса DataProcessing я как раз и вызываю необходимые методы других классов, которые как раз и выполняют необходимый функционал. Просто для раскрытия темы не было необходимости в демонстрации других классов

      А в приведённом Вами примере я не вижу, в каком месте можно «включать» и «выключать» выполнение определённой функции. Если я не прав, то поясните пожалуйста


      1. Dhwtj
        05.08.2025 16:44

        На эксперименты с максимальной гибкостью вам хватит такого. Создаёте какой угодно скрипт, но с ограничениями для безопасности. И можете хоть с клавиатуры его вводить и отправлять на исполнение.

        # Скрипт - это строка, описывающая композицию.
        # Функции будут вызваны в момент выполнения.
        script = "format_report(process_data(load_data('/my/data')))"
        
        # Реальные функции, которые будут доступны "внутри" eval
        def _load_data(path: str): return [1, 2, -5, 10, -3]
        def _process_data(data: list): return [x for x in data if x > 0]
        def _format_report(data: list): return f"Report: sum = {sum(data)}"
        
        # Исполнитель
        def run_script_with_eval(script_string: str):
            # Создаем "окружение" для eval, чтобы он не имел доступа ко всему.
            # Мы явно указываем, какие имена (функции) ему разрешено видеть.
            allowed_context = {
                'load_data': _load_data,
                'process_data': _process_data,
                'format_report': _format_report,
            }
        
            # eval(source, globals, locals)
            # Передаем наш словарь как "глобальное" окружение для этой команды.
            return eval(script_string, allowed_context)
        
        # Ключевой шаг: передаем словарь, где __builtins__ ПУСТОЙ.
        # Это блокирует доступ ко всем встроенным функциям.
        secure_globals = {
            "__builtins__": {}, 
            **allowed_context  # Добавляем наши разрешенные функции
        }
            
        
        return eval(script_string, secure_globals)


        1. r3m4k Автор
          05.08.2025 16:44

          Хороший подход, спасибо


  1. yaroslavp
    05.08.2025 16:44

    Строчки жалко? Как это дебажить?


    1. r3m4k Автор
      05.08.2025 16:44

      Строчки жалко?

      Можете пояснить, что Вы имеете ввиду?

      Как это дебажить?

      Как по мне, можно добавить дополнительный вывод (в консоль или соответствующий лог, неважно) и в нужных местах точки останова. Должно быть достаточно