Не работает setattr в дескрипторе Python

Пишу класс Triangle для работы с треугольником. Вот код:

class Triangle(Figure):
    a = SideDescriptor()
    b = SideDescriptor()
    c = SideDescriptor()

    def __init__(self, a: int | float, b: int | float, c: int | float) -> None:
        self.a = a
        self.b = b
        self.c = c

    @property
    def area(self) -> float:
        semiperimeter = self.perimeter / 2
        return (semiperimeter * (semiperimeter - self.a) * (semiperimeter - self.b) * (semiperimeter - self.c)) ** 0.5

    @property 
    def perimeter(self) -> int | float:
        return self.a + self.b + self.c

Каждая из сторон проходит проверку в SideDescriptor. Если значение негативное или не является числом, то вызывает исключение ValueError. Вот код для дескриптора:

class SideDescriptor:
    def __set_name__(self, owner, name):
        self.param_name = name

    def __get__(self, instance, owner):
        return getattr(instance, self.param_name)

    def __set__(self, instance, value):
        if not isinstance(value, (int, float)):
            raise ValueError('Side value must be integer or float instance!')
        if value < 0:
            raise ValueError('Side value can not negative!')
        self.param_name = value

Так он работает. Но стоит мне заменить self.param_name = value на setattr(instance, self.param_name, value), то происходит ошибка:

Traceback (most recent call last):
  File "/Users/polina/ProGeo/app/triangle.py", line 59, in <module>
    triangle = Triangle(3, 2, 5)
               ^^^^^^^^^^^^^^^^^
  File "/Users/polina/ProGeo/app/triangle.py", line 28, in __init__
    self.a = a
    ^^^^^^
  File "/Users/polina/ProGeo/app/triangle.py", line 19, in __set__
    setattr(instance, self.param_name, value)
  File "/Users/polina/ProGeo/app/triangle.py", line 19, in __set__
    setattr(instance, self.param_name, value)
  File "/Users/polina/ProGeo/app/triangle.py", line 19, in __set__
    setattr(instance, self.param_name, value)
  [Previous line repeated 743 more times]
  File "/Users/polina/ProGeo/app/triangle.py", line 15, in __set__
    if not isinstance(value, (int, float)):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded in __instancecheck__

Не могу понять, в чем проблема. Почему так не работает?


Ответы (1 шт):

Автор решения: artur1214

Вы не совсем понимаете, как работает дескриптор.

Вам не нужно вызывать setattr для instance.

При получении значения дескриптора, автоматически вызывается __get__ дескриптора. То, что вернет эта функция и будет значением дескриптора.

При установке значения дескриптора вам нужно внутри дескриптора установить то значение, которое будет возвращать метод __get__

Когда вы используете setattr(instance, self.param_name, value) вы устанавливаете значение напрямую внутри Trinagle, при этом вызывается __set__ дескриптора, он после проверок вызывает setattr напрямую в объекте triangle, это вызывает __set__ дескриптора, что опять вызывает setattr и так по кругу, пока рекурсия не выйдет за максимальную глубину.

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

Здесь стоит заметить, что return getattr(instance, self.param_name) в методе __get__ тоже является не верным. Когда код дойдет до получения значения дескриптора, вы так же получите RecursionError.

Причина такая же:

При получении значения вызывается __get__ дескриптора, оно вызывает getattr у instance, что вызывает __get__ для дескриптора, которое вызывает getattr и т.д.

Рассмотрим пример валидного дескриптора:


class SideDescriptor:

    def __set_name__(self, owner: type['Triangle'], name: str):
        self.param_name = name

    def __init__(self, value=None):
        self.value = value

    def __set__(self, instance: 'Triangle', value: float | int):

        if not isinstance(value, (int, float)):
            raise ValueError('Side value must be integer or float instance!')
        if value < 0:
            raise ValueError('Side value can not negative!')

        sides = instance.sides
        del sides[self.param_name]
        ready_sides = len(list(filter(lambda x: sides[x] != 0,sides) ))
        s = sum(sides.values())
        if ready_sides == 2 and s != 0 and s <= value:
            raise ValueError("the length of the third side cannot be greater "
                             "than the sum of the other two sides")
        self.value = value

    def __get__(self, instance: 'Triangle', owner: type['Triangle']):

        return self.value
    


class Triangle:
    a = SideDescriptor()
    b = SideDescriptor()
    c = SideDescriptor()

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    @property
    def area(self):
        semiperimeter = self.perimeter / 2
        return (semiperimeter * (semiperimeter - self.a) * (
                semiperimeter - self.b) * (semiperimeter - self.c)) ** 0.5

    @property
    def perimeter(self) -> int | float:
        return self.a + self.b + self.c

    @property
    def sides(self) -> dict:
        return dict({'a': self.a or 0, 'b': self.b or 0, 'c': self.c or 0})

Как видите, здесь __get__ дескриптора просто возвращает значение поля value, хранящегося в дескрипторе

Само значение value задается в __init__, дескриптора

__set__ же просто устанавливает value в дескрипторе, чтобы потом его мог вернуть __get__.

Но зачем же нам instance?

получение экземпляра instance (тот объект в котором установлен дескриптор) позволяет нам проводить дополнительные проверки связанные с тем экземпляром, для которого мы создали дескриптор.

Например я добавил в код проверку на соответствие условию существования треугольника для которого необходимо получить не только новое значение внутри дескриптора, но и значения 2х других сторон треугольника.

Подведем итог.

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

Необходимо лишь изменить то значение, которое хранится внутри самого дескриптора. instance же можно использовать для проведения проверок связанных с другими значениями внутри объекта тругольника. Либо установки других значений внутри объекта (но будьте осторожны, чтобы не нарваться на другой дескриптор и опять получить RecursionError)

→ Ссылка