Как исправить порядок унаследованных подклассов в dataclass Python

Вопрос или проблема

Я пытаюсь создать Dataclass для координат широты и долготы, используя наследуемые подклассы широты и долготы. Но порядок наследуемых подклассов неправильный.

from dataclasses import dataclass


# Пользовательский TypeValidator https://gist.github.com/rnag/db6bf83d9ca19dfe897d6ccabd4e2570
from validators import TypeValidator

@dataclass
class Latitude:
    lat: float | int = TypeValidator()

    def __post_init__(self):
        if not -90 <= self.lat <= 90:
            raise ValueError('Широта должна быть от -90 до 90')


@dataclass
class Longitude:
    lon: float | int = TypeValidator()

    def __post_init__(self):
        if not -180 <= self.lon <= 180:
            raise ValueError('Долгота должна быть от -180 до 180')

@dataclass
class LatLonPoint(Latitude,Longitude):
    pass

Но он ставит долготу первым:

print(LatLonPoint(1,1))

LatLonPoint(lon=1, lat=1)

Одна из причин создания его таким образом заключалась в том, что я пытался исправить валидацию после инициализации, создав подклассы для широты и долготы. Я думал, что это будет означать, что подкласс переинициализируется каждый раз при обновлении значения. Но это привело к проблеме с порядком. Я немного почитал о MRO. Но существует ли способ исправить порядок без добавления kw_only=True?

Вы оба подходите к этому неправильно и смешиваете концепции.

Вряд ли вам понадобятся специализированные подклассы для широты и долготы для каких-либо практических целей (хотя, да, они могут иметь некоторое применение).

Во-вторых: это не то, как работает объектно-ориентированное наследование: ваш класс либо ЯВЛЯЕТСЯ тем, чем являются их суперклассы — в данном случае это означает, что ваш LatLonPoint должен одновременно “быть” как координатой Latitude, так и Longitude. Либо он может владеть другими классами в отношении — тогда ваш LatLonPoint будет иметь широту и долготу.

Вот к чему вам следует стремиться.

Еще одна вещь, которая не имеет смысла: вы хотите, чтобы широта БЫЛА значением, а не имела значение “.lat”. Итак, если вы действительно хотите этого добиться, вы можете создать классы, которые наследуют от float — и тогда забудьте о dataclasses: это механизм для подавления шаблонного кода во множестве случаев использования — но не предназначен для тех случаев, когда нужно специализировать класс.

Без отдельных классов широты и долготы вы можете использовать широту и долготу в качестве свойств в LatLongPoint — это будет более короткий и прямой способ работать с этим в Python. Эти свойства реализуемы как dataclasses, но другие инструменты, такие как проверка типов, могут выдать предупреждение, так как поля не будут содержать правильный экземпляр float как значение по умолчанию.

Наследование от float имеет свои особенности и обычно считается более продвинутой темой — но это будет ближе к тому, что вы делали.

И, чтобы убедиться, что это понято: нет смысла наследовать широту и долготу, как вы это делаете.

from dataclasses import dataclass, field


# Общее специализированное значение float поможет избежать дублирования кода:
class CoordPoint(float):
    min = 0.0
    max = 0.0
    name = "Абстрактная координата"
    def __new__(cls, value):
        instance = super().__new__(cls, value)
        instance._check_limits()
        return instance
    
    def _check_limits(self):
        # мы _ЯВЛЯЕМСЯ_ float, поэтому "self" можно использовать напрямую для значения:
        if not self.min <= self <= self.max: 
            raise ValueError(f"{self.name} должно быть от {self.min} до {self.max}")
    # вы можете специализировать арифметические операции, такие как __add__ и __sub__,
    # чтобы операции могли выполняться только с одним и тем же классом,
    # и также возвращали экземпляр правильного класса.
    # (так что сложение объектов долготы и широты не допускается)
    # Это легко сделать, если это необходимо, но будет излишним для 99.9% случаев использования
    
    # пример:
    def __add__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError("...")
        return type(self)(super().__add__(other))
    
    def __repr__(self):
        return f"{self.__class__.__name__}({float(self)})"
    
    
# Теперь мы используем силу О.О., чтобы иметь
# классы широты и долготы с минимальным кодом и шаблонами:

class Latitude(CoordPoint):
    min = -90
    max = 90
    name = "Широта"
    

class Longitude(CoordPoint):
    min = -180
    max = 180
    name = "Долгота"


@dataclass
class LatLonPoint:
    # ничего не наследуем, просто указываем типы данных и 
    # позволяйте dataclass работе своей магии!
    lat: Latitude
    lon: Longitude
    
    # dataclasses не пытаются принудительно преобразовать заданные входные значения
    # в экземпляры типа, заданного для каждого поля.
    # этот код после инициализации может это сделать:
    def __post_init__(self):
        for field_name, field in self.__dataclass_fields__.items():
            setattr(self, field_name, field.type(getattr(self, field_name)))

Ответ или решение

Как исправить порядок наследуемых подклассов в Python Dataclass

Ваша проблема связана с тем, что при создании Dataclass, представляющего координаты широты и долготы, порядок аргументов меняется из-за иерархии наследования. Рассмотрим детально, как решение можно оптимизировать и скорректировать.

1. Суть проблемы

В вашем коде вы создали два подкласса: Latitude и Longitude, и потом наследовали их в классе LatLonPoint. Однако при создании экземпляра класса LatLonPoint, порядок параметров, выводимый на экран, всегда ставит lon перед lat:

print(LatLonPoint(1, 1))  # Вывод: LatLonPoint(lon=1, lat=1)

Это связано с тем, что Python использует метод разрешения порядка (MRO, Method Resolution Order) в случае множественного наследования, который устанавливает порядок инициализации и доступ к атрибутам.

2. Предложение по улучшению кода

Вместо использования множественного наследования, вы можете рассмотреть возможность создания класса LatLonPoint, который будет включать объекты Latitude и Longitude как свои атрибуты. Это создаст более чистую архитектуру и избежит проблем с порядком атрибутов.

Вот как это можно реализовать:

from dataclasses import dataclass

class CoordPoint(float):
    min: float
    max: float
    name: str = "Abstract coord point"

    def __new__(cls, value):
        instance = super().__new__(cls, value)
        instance._check_limits()
        return instance

    def _check_limits(self):
        if not self.min <= self <= self.max:
            raise ValueError(f"{self.name} должен быть в диапазоне {self.min} до {self.max}")

class Latitude(CoordPoint):
    min = -90
    max = 90
    name = "Широта"

class Longitude(CoordPoint):
    min = -180
    max = 180
    name = "Долгота"

@dataclass
class LatLonPoint:
    lat: Latitude
    lon: Longitude

    def __post_init__(self):
        # Поскольку это не преобразует тип, используем явное создание классов
        self.lat = Latitude(self.lat)
        self.lon = Longitude(self.lon)

# Пример использования
point = LatLonPoint(1, 1)
print(point)  # Теперь порядок будет: LatLonPoint(lat=1.0, lon=1.0)

3. Пояснение изменений

  1. Отказ от множественного наследования: Класс LatLonPoint больше не наследует от Latitude и Longitude, что избавляет от проблем с порядком атрибутов.

  2. Логика инициализации: В методе __post_init__ каждое значение преобразуется в соответствующий тип (например, Latitude или Longitude), что гарантирует, что какие-либо попытки передать некорректные значения будут перехвачены.

  3. Чистота кода: Упрощенный подход повысит читаемость вашего кода и упростит его поддержку.

Заключение

Следуя этим рекомендациям, вы сможете исправить порядок своих атрибутов и улучшить структуру вашего кода. Отклонение от множественного наследования, как показано выше, — это стандартный прием в ООП, который часто помогает избегать проблем с порядком инициализации.

Оцените материал
Добавить комментарий

Капча загружается...