У меня есть код Python, который имеет следующую форму:
from dataclasses import dataclass
@dataclass
class Foo_Data:
foo: int
class Foo_Processor:
def process(self, data: Foo_Data): ...
class Foo_Loader:
def load(self, file_path: str) -> Foo_Data: ...
@dataclass
class Bar_Data:
bar: str
class Bar_Processor:
def process(self, data: Bar_Data): ...
class Bar_Loader:
def load(self, file_path: str) -> Bar_Data: ...
У меня есть несколько экземпляров такой настройки данных/процессора/загрузчика, и все классы имеют одинаковые сигнатуры методов по модулю конкретного семейства классов (Foo, Bar и т. д.). Существует ли питонический способ формализовать эти отношения между классами, чтобы обеспечить аналогичную структуру, если я решу создать семейство классов Spam_Data
, Spam_Processor
и Spam_Loader
? Например, я хочу, чтобы что-то Spam_Processor
имело метод process
, который принимает аргумент типа Spam_Data
. Есть ли способ каким-то образом добиться этой стандартизации с помощью абстрактных классов, универсальных типов или какой-либо другой структуры?
Я пробовал использовать абстрактные классы, но mypy правильно указывает, что наличие всех классов *_Data подклассов абстрактного Data
класса, а также то, что все классы *_Processor являются подклассами абстрактного Processor
класса, нарушает принцип подстановки Лискова, поскольку каждый процессор предназначен только для соответствующего класса данных (т. е. Foo_Processor
не может обрабатывать Bar_Data
, но можно было бы ожидать, что это возможно, если у этих классов есть суперклассы Processor
и Data
, которые совместимы таким образом).
Почему, например, Foo_Processor
вообще является классом, а не простой process_foo
функцией?
Я упростил код, чтобы попытаться прояснить мой вопрос и облегчить его поиск/понимание другим, оказавшимся в аналогичной ситуации. @PaulWilson, @chepner, я не включил другой код/файлы, которые помогли бы показать, что другой код может создавать экземпляры Foo_Data
, а также Foo_Processor
на самом деле имеет атрибуты, которые устанавливаются, а затем используются для обработки. Вот почему я решил сохранить все это отдельно и как классы, а не как функции.
Вы можете использовать абстрактные базовые классы (ABC) с дженериками. Таким образом вы можете определить общий интерфейс, гарантируя при этом безопасность типов:
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar
# generic type variable for Data
T = TypeVar('T', bound='BaseData')
@dataclass
class BaseData(ABC):
pass
class BaseProcessor(ABC, Generic[T]):
@abstractmethod
def process(self, data: T) -> None:
pass
class BaseLoader(ABC, Generic[T]):
@abstractmethod
def load(self, file_path: str) -> T:
pass
Теперь вы можете определить свои конкретные классы
@dataclass
class Foo_Data(BaseData):
foo: int
class Foo_Processor(BaseProcessor[Foo_Data]):
def process(self, data: Foo_Data) -> None: ...
class Foo_Loader(BaseLoader[Foo_Data]):
def load(self, file_path: str) -> Foo_Data: ...
@dataclass
class Bar_Data(BaseData):
bar: str
class Bar_Processor(BaseProcessor[Bar_Data]):
def process(self, data: Bar_Data) -> None: ...
class Bar_Loader(BaseLoader[Bar_Data]):
def load(self, file_path: str) -> Bar_Data: ...
Написание кода таким образом сочетает в себе преимущества общего интерфейса с безопасностью типов.
ABC гарантирует, что подклассы реализуют необходимые методы, обеспечивая согласованную структуру.
Обобщенные шаблоны позволяют выполнять операции с конкретным типом, улучшая читаемость и удобство обслуживания кода.
В качестве подтверждения с mypy:
mypy script.py
Success: no issues found in 1 source file
Можете ли вы объединить каждый класс _Processor, _Loader и _Data? Вы можете использовать абстрактный базовый класс для определения отношений. Что-то вроде
myclass
как ABC сload
иprocess
как абстрактными методами, затемclass Foo(myclass)
и определите там конкретные данные, методы загрузки и обработки.