Вопрос или проблема
Я пытаюсь создать 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. Пояснение изменений
-
Отказ от множественного наследования: Класс
LatLonPoint
больше не наследует отLatitude
иLongitude
, что избавляет от проблем с порядком атрибутов. -
Логика инициализации: В методе
__post_init__
каждое значение преобразуется в соответствующий тип (например,Latitude
илиLongitude
), что гарантирует, что какие-либо попытки передать некорректные значения будут перехвачены. -
Чистота кода: Упрощенный подход повысит читаемость вашего кода и упростит его поддержку.
Заключение
Следуя этим рекомендациям, вы сможете исправить порядок своих атрибутов и улучшить структуру вашего кода. Отклонение от множественного наследования, как показано выше, — это стандартный прием в ООП, который часто помогает избегать проблем с порядком инициализации.