Не работает 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 шт):
Вы не совсем понимаете, как работает дескриптор.
Вам не нужно вызывать 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
)