Как правильно использовать дескрипторы в качестве полей в классах данных Python?

#python #python-dataclasses #python-descriptors

Вопрос:

Я играл с датаклассами python и задавался вопросом: каков самый элегантный или самый питонический способ создания одного или нескольких дескрипторов полей?

В приведенном ниже примере я определяю класс Vector2D, который следует сравнивать по его длине.

 from dataclasses import dataclass, field
from math import sqrt

@dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(init=False)
    
    def __post_init__(self):
        type(self).length = property(lambda s: sqrt(s.x**2 s.y**2))

Vector2D(3,4) > Vector2D(4,1) # True
 

Хотя этот код работает, он затрагивает класс каждый раз, когда создается экземпляр, существует ли более читаемый / менее хакерский / более подходящий способ совместного использования классов данных и дескрипторов?

Просто наличие длины в качестве свойства , а не поля, будет работать, но это означает, что я должен написать __lt__ , et.al… сам по себе.

Другое решение, которое я нашел, столь же непривлекательно:

 @dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(init=False)
    
    @property
    def length(self):
        return sqrt(self.x**2 self.y**2)
    
    @length.setter
    def length(self, value):
        pass
 

Введение установщика без операций необходимо, поскольку, по-видимому, метод инициализации, созданный в классе данных, пытается присвоить длину, даже если значения по умолчанию нет и оно явно устанавливается init=False

Конечно, должен быть лучший способ, верно?

Комментарии:

1. Мне интересно, просто ли вы не пересекаете тонкую границу между классом данных, т. Е. какой — то тонкой оболочкой вокруг данных, и правильным классом, представляющим сущность с операциями над собой -в вашем случае Вектор ( пример Unity3D ).

2. Я бы задал вопрос, хотите ли вы сделать этот класс упорядоченным. Рассмотрим order=True это eq=True , но вы, вероятно, не хотите считать два вектора равными, если они имеют одинаковую длину. Учтите, что вы все еще можете сортировать элементы на основе length чего-то подобного sorted(vectors, key=lambda v: v.length) и аналогично для других целей. В этом случае я бы сохранил length как свойство, а не как поле.

3. «Более читаемый / менее хакерский способ» — это правильная реализация __lt__ , ИМО. Любому, кто читает ваш код, становится намного яснее, как именно сравниваются два Vector2D экземпляра. И таким образом, вам больше не понадобятся length поле класса данных и length.setter метод, только length свойство.

4. Я впечатлен быстрыми ответами, и я признаю, что вы поднимаете обоснованные вопросы. Вероятно, включение этого в класс данных только для того, чтобы сделать определение класса более кратким, на самом деле не улучшит читабельность.

Ответ №1:

Возможно, я не отвечу на ваш точный вопрос, но вы упомянули, что причина, по которой вы не хотели иметь длину в качестве свойства и поля «не», заключалась в том, что вам пришлось бы

напиши __lt__ , et.al сам по себе

Хотя вам действительно нужно реализовывать __lt__ самостоятельно, вы можете на самом деле обойтись без реализации именно этого

 from functools import total_ordering
from dataclasses import dataclass, field
from math import sqrt

@total_ordering
@dataclass
class Vector2D:
    x: int
    y: int

    @property
    def length(self):
        return sqrt(self.x ** 2   self.y ** 2)

    def __lt__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented

        return self.length < other.length

    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented

        return self.length == other.length


print(Vector2D(3, 4) > Vector2D(4, 1))
 

Причина, по которой это работает, заключается в том, что total_ordering просто добавляет все другие методы равенства, основанные на __eq__ и __lt__

Комментарии:

1. В вашем примере, возможно, неправильного сравнения, они действительно имеют одинаковую длину?

2. Да, это правда, я думаю, что это деталь реализации, если Vector2D(0, 1) == Vector2D(1, 0)

3. Это желаемое поведение из OP: «В приведенном ниже примере я определяю класс Vector2D, который следует сравнивать по его длине».

4. Я не думал о чем-то подобном total_ordering . Хотя это не отвечает на точный вопрос, который я задал, это избавляет меня от множества избыточных функций сравнения и гораздо более читабельно, чем мой исходный код. Я соглашусь с таким подходом, большое спасибо!

Ответ №2:

Я не думаю, что приведенный вами пример является хорошим примером использования того, что вы пытаетесь сделать. Тем не менее, для полноты картины, вот возможное решение вашей проблемы:

 @dataclass(order=True)
class Vector2D:
    x: int = field(compare=False)
    y: int = field(compare=False)
    length: int = field(default=property(lambda s: sqrt(s.x**2 s.y**2)), init=False)
 

Это работает, потому dataclass что устанавливает значения по умолчанию в качестве значений атрибутов класса, если только значение не является списком, диктом или набором.