class MyClass:
          def __init__(self, value):
            self._value = value
          @property
          def value(self):
            return self._value
      
      Попытка присвоить значение `obj.value = new_value` вызовет `AttributeError`.
    Есть несколько способов сделать атрибуты объектов доступными только для чтения в Python, предотвращая их изменение через сеттеры:
property):
      Это наиболее Pythonic подход.  Вы определяете свойство с помощью декоратора @property и предоставляете только геттер (метод для получения значения атрибута), но не определяете сеттер (метод для установки значения).
class MyClass:
    def __init__(self, value):
        self._my_attribute = value  # Используем соглашение об именовании
    @property
    def my_attribute(self):
        return self._my_attribute
# Пример использования:
obj = MyClass(10)
print(obj.my_attribute)  # Вывод: 10
try:
    obj.my_attribute = 20
except AttributeError as e:
    print(e) # Вывод: can't set attribute
В этом примере, атрибут my_attribute можно получить, но нельзя изменить напрямую.  Попытка присвоить ему новое значение приведет к AttributeError.
Соглашение об именовании (начинать имя атрибута с _) говорит о том, что атрибут предназначен для внутреннего использования и не должен изменяться напрямую извне класса. Это, конечно, не делает атрибут "полностью приватным" в смысле ограничения доступа на уровне языка, но служит четким сигналом для других разработчиков.
__slots__:
      __slots__ не делает атрибуты неизменяемыми напрямую, но если вы не включите атрибут в __slots__, то он не сможет быть добавлен динамически.  Это полезно для контроля атрибутов, которые могут существовать в вашем объекте.
class MyClass:
    __slots__ = ['my_attribute']
    def __init__(self, value):
        self.my_attribute = value
obj = MyClass(10)
print(obj.my_attribute)
try:
    obj.new_attribute = 20 # Attempt to add a non-declared attribute
except AttributeError as e:
    print(e) # Output: 'MyClass' object has no attribute 'new_attribute'
Важно отметить, что __slots__ в первую очередь предназначен для оптимизации использования памяти, а не для защиты атрибутов.  Он предотвращает создание __dict__ для каждого экземпляра класса, что экономит память, но не делает атрибуты неизменяемыми в принципе. В данном примере my_attribute все еще можно изменить, просто нельзя добавить новый атрибут.
Можно добавить дополнительную логику для предотвращения изменений в сеттере (если он определен), например, выбрасывать исключение, если кто-то пытается установить новое значение.
class MyClass:
    def __init__(self, value):
        self._my_attribute = value
    @property
    def my_attribute(self):
        return self._my_attribute
    @my_attribute.setter
    def my_attribute(self, value):
        raise AttributeError("Атрибут доступен только для чтения")
# Пример использования:
obj = MyClass(10)
print(obj.my_attribute)
try:
    obj.my_attribute = 20
except AttributeError as e:
    print(e) # Вывод: Атрибут доступен только для чтения
В данном примере, хотя сеттер и существует, он всегда выбрасывает исключение, предотвращая любое изменение атрибута. Это дает более явный контроль над тем, что происходит при попытке установить атрибут.
attrs (внешняя библиотека):
      Библиотека attrs предоставляет удобный способ определения классов данных с автоматической генерацией методов и возможностью определения атрибутов только для чтения.
import attr
@attr.s(frozen=True) # frozen=True делает все атрибуты неизменяемыми
class MyClass:
    my_attribute = attr.ib()
# Пример использования:
obj = MyClass(my_attribute=10)
print(obj.my_attribute)
try:
    obj.my_attribute = 20
except attr.exceptions.FrozenInstanceError as e:
    print(e) # Вывод: cannot assign to field 'my_attribute'
attrs требует установки (pip install attrs) и предлагает мощные инструменты для управления атрибутами классов данных.
    Выбор подхода зависит от конкретных требований вашего проекта.  property - наиболее распространенный и Pythonic способ. __slots__ - для оптимизации памяти и контроля динамических атрибутов.  attrs - для создания неизменяемых классов данных. Добавление логики в сеттер для property - наиболее гибкий подход.