Комментарии 24
Класс делает всё: читает, классифицирует, рисует, анализирует. Лучше разделить на:
* 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)
Из класса DataProcessing я как раз и вызываю необходимые методы других классов, которые как раз и выполняют необходимый функционал. Просто для раскрытия темы не было необходимости в демонстрации других классов
А в приведённом Вами примере я не вижу, в каком месте можно «включать» и «выключать» выполнение определённой функции. Если я не прав, то поясните пожалуйста
На эксперименты с максимальной гибкостью вам хватит такого. Создаёте какой угодно скрипт, но с ограничениями для безопасности. И можете хоть с клавиатуры его вводить и отправлять на исполнение.
# Скрипт - это строка, описывающая композицию.
# Функции будут вызваны в момент выполнения.
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)
Строчки жалко? Как это дебажить?
Всегда думала, что это классика в питоне, селект из словаря. Но понимаю радость первооткрывателя 😁
Если работает, то ок...но чёт мне не нравится, а разбираться лень.. вот self._parameters['Raw_Data'] - с телефона не вижу откуда вообще это взялось.
В общем, то что сейчас кажется элегантным - скорее всего забудется/поломается в самый неприятный момент.
Рекомендую скопировать код и попросить chatGPT написать 3-5 вариантов с обоснованием рекомендованного. Предложить разные паттерны под эту задачу GPT точно может.
Я думал вы просто сделаете словарь вида
Key: [bool, funk]
Потом когда будете считывать данные, то будете менять значения bool, после в чего в цикле выполнять funk если bool
Тоже неплохой вариант, но для этого надо из класса GUI «знать» нужные методы из класса DataProcessing, что является не совсем корректным паттерном, на мой взгляд.
В любом случае необходимо где-то сохранить соответствие функций и чекбоксов. В вашем паттерне это сделано в классе DataProcessing, но можно и наоборот.
Во фронтенде предложенный @brizol паттерн повсеместно применяется в кнопках, то самое <button onclick="myFunction()">Click me</button>
Так а какую "кучу флагов" то удалось заменить? В примере из всего 3. А в целом как-то путано на мой взгляд.
Словари в таких задач действительно удобны. Единственное что появится вопрос сохранения состояний при перезапуске (если оно нужно).
И тогда можно прийти к конфиг файлам, куда есть возможность как добавить так и изменить переменные. Это ещё и полезно, если будут ошибки, то можно будет не опасаться того что состояния вернулись к стандартным.
Словари можно типизировать, иначе они рано или поздно разъедутся
from typing import TypedDict, Any
class RawDataPlottingParams(TypedDict):
plotting_raw_data: bool
plotting_init_data: bool
plotting_buffers_data: bool
kwargs: dict[str, Any]
class ProcessingParameters(TypedDict):
Raw_Data: RawDataPlottingParams
Для RawData возможно уместнее
defaultdict
со значением по умолчаниюfalse,
чтобы при отсутствующем ключе все не падало.Лучше всего использовать
dataclass
,pydantic-settings
илиyaml/toml
конфиг
Прочтя о замене флагов словарями, подумал о замене множества очень похожих ветвлений массивы условий и циклы по ним. Оказалось, речь о слое конфига.
Немного не по теме, но хочу поделиться структурой конфига, которую я использовал в одном из проектов по сбору данных. Тоже словари, тоже разделение взаимосвязей, вдруг пригодится. Фидбек тоже приветствуется.
Скрытый текст
Задача - конфигурация платы сбора данных и подключенных к ней датчиков через аналоговые и цифровые порты без изменения кода программы.
Конфигурация IO хранится в JSON файле, как вложенный словарь. Логика инициализации датчиков, обработка и сбор данных осуществляется через класс BoardIO. В коде есть методы, которые принимают DeviceIO и используют подходящию логику чтения/записи в зависимости от типа датчика/терминала.
# Создаем класс с аттрибутами для инициализации датчика или устройства
@dataclass(frozen=True)
class DeviceIO:
name: str
ch_type: Literal["analog", "digital"]
io: Literal["in", "out"]
terminals: list[int]
channel: int
...
# Создаем класс с устройствами, к которым обращаемся по имени в коде
@dataclass(slots=True)
class BoardIO:
torque_l: DeviceIO
torque_r: DeviceIO
sp_counter: DeviceIO
encoder_r: DeviceIO
...
# При инициализации загружаем словарь с конфигом из JSON для всех устройств датчика
def _load_config(self, config_channels: str) -> None:
with open(config_channels, encoding="utf_8") as file:
daq_config = json.load(file)
# Присваиваем аттрибуты из JSON для каждого устройства
for device, params in daq_config["channels"].items():
if device in self.__dataclass_fields__:
new_class = DeviceIO(name=device, **params)
setattr(self, device, new_class)
else:
logger.error(
"Naming error in the '%s'. '%s' is not defined"
"in the dataclass, so it was not initialised.",
config_channels,
device,
)
Я не совсем понимаю аннотацию через dir, у вас создан пользовательский тип?
Как я заменил кучу флагов двумя словарями в Python