Инкапсуляция и property

Теория

У товара есть правила: цена не бывает отрицательной, а остаток меняется только при резерве и поступлении на склад. С публичными атрибутами за этим никто не следит. Любой код может написать product.price = Decimal("-100"), и ошибка всплывёт через неделю — в отчёте или в чеке. Инкапсуляция — это когда объект сам отвечает за корректность своих данных. Внутреннее состояние скрыто, а снаружи доступны только разрешённые действия, которые проверяют входные данные.

Запрета доступа, как в Java, в Python нет — есть соглашения. Имя с одним подчёркиванием (_price) значит «внутреннее, снаружи не трогать». Python его не защищает, но программисты это соглашение соблюдают. Два подчёркивания (__stock) включают искажение имени (name mangling): Python переименовывает атрибут в _Product__stock. Это защищает от случайного совпадения имён в классах-наследниках, а не от доступа. Главный инструмент — property: метод, который снаружи выглядит как атрибут. Чтение product.price вызывает геттер, присваивание product.price = x — сеттер с проверкой. Код, который пользуется классом, не меняется, а проверки начинают работать.

Наглядно

Таблица 1. Как в Python обозначают доступ к атрибутам.

Имя Что значит Что делает Python Когда использовать
price публичный атрибут ничего данные, которые можно менять как угодно
_price внутренний — «снаружи не трогать» ничего, это соглашение хранение данных за property, вспомогательные методы
__stock «частный» для этого класса переименовывает в _Product__stock редко: чтобы наследник случайно не перезаписал атрибут
@property price атрибут с логикой чтение и запись вызывают методы проверка значений, вычисляемые поля, поля только для чтения

Таблица 2. Что происходит при работе со свойством price.

Код Что вызывается
product.price геттер — метод под @property
product.price = x сеттер — метод под @price.setter; без сеттера — AttributeError
self.price = price в __init__ тоже сеттер: проверка срабатывает и при создании
self._price обычный атрибут, где сеттер хранит значение

На практике

Задача: товар не должен принимать отрицательную цену — ни при создании, ни при изменении. Остаток должен меняться только через резерв.

Шаг 1. Видим проблему публичного атрибута

Публичный атрибут принимает любое значение:

python
from decimal import Decimal


class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price


mouse = Product("Мышь", Decimal("1499.00"))
mouse.price = Decimal("-100")
print(mouse.price)

Результат:

вывод
-100

Шаг 2. Превращаем цену в свойство

Значение храним во внутреннем _price, а читаем через @property. Снаружи запись mouse.price не изменилась:

python
class Product:
    def __init__(self, name, price):
        self.name = name
        self._price = price

    @property
    def price(self):
        return self._price


mouse = Product("Мышь", Decimal("1499.00"))
print(mouse.price)

Результат:

вывод
1499.00

Шаг 3. Добавляем сеттер с проверкой

Сеттер называется так же, как свойство, и помечается @price.setter. Он проверяет значение и записывает его в _price:

python
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError(f"цена не может быть отрицательной: {value}")
        self._price = value


mouse = Product("Мышь", Decimal("1499.00"))
mouse.price = Decimal("1299.00")
print(mouse.price, mouse.__dict__)
mouse.price = Decimal("-100")

Результат:

вывод
1299.00 {'name': 'Мышь', '_price': Decimal('1299.00')}
ValueError: цена не может быть отрицательной: -100

В __init__ написано self.price = price, а не self._price = price. Присваивание идёт через сеттер, поэтому проверку нельзя обойти и при создании:

python
Product("Коврик", Decimal("-5"))

Результат:

вывод
ValueError: цена не может быть отрицательной: -5

Шаг 4. Делаем остаток только для чтения

Свойство без сеттера читается, но не присваивается. Остаток меняют только методы, где есть проверки:

python
class Product:
    def __init__(self, name, stock):
        self.name = name
        self._stock = stock

    @property
    def stock(self):
        return self._stock

    @property
    def is_available(self):
        return self._stock > 0

    def reserve(self, quantity):
        if quantity > self._stock:
            raise ValueError(f"{self.name}: на складе {self._stock}, просят {quantity}")
        self._stock -= quantity


mouse = Product("Мышь", 3)
mouse.reserve(3)
print(mouse.stock, mouse.is_available)
mouse.stock = 100

Результат:

вывод
0 False
AttributeError: property 'stock' of 'Product' object has no setter

is_available — вычисляемое свойство: значение нигде не хранится, а считается при каждом чтении. Без скобок, как обычный атрибут.

Шаг 5. Смотрим, что делают два подчёркивания

__stock внутри класса Python переименовывает в _Product__stock. Снаружи под исходным именем атрибута нет, а присваивание obj.__stock создаёт новый атрибут:

python
class Product:
    def __init__(self, name, stock):
        self.name = name
        self.__stock = stock

    @property
    def stock(self):
        return self.__stock


pad = Product("Коврик", 12)
print(pad.__dict__)
pad.__stock = 0
print(pad.stock, pad.__dict__)

Результат:

вывод
{'name': 'Коврик', '_Product__stock': 12}
12 {'name': 'Коврик', '_Product__stock': 12, '__stock': 0}

Остаток не изменился: снаружи класса имя не искажается, и pad.__stock — просто другой атрибут.

Частые ошибки

  • Сеттер пишет в само свойство. self.price = value внутри сеттера price снова вызывает сеттер — и так до RecursionError: maximum recursion depth exceeded. Хранить значение нужно в другом атрибуте: self._price = value.
  • __init__ обходит проверку. self._price = price в __init__ пропускает сеттер, и товар создаётся с отрицательной ценой. В __init__ присваивают через свойство: self.price = price.
  • obj.__x = ... снаружи класса. Имя искажается только внутри класса. Снаружи создаётся новый атрибут __x, а настоящий (_Class__x) не меняется — и ошибки нет.
  • Геттеры и сеттеры «как в Java». get_price()/set_price() для каждого поля в Python не пишут. Начинают с публичного атрибута, а проверку добавляют через property, когда она нужна. Внешний код при этом не меняется.
  • Скобки у свойства. mouse.is_available() — ошибка: свойство уже вернуло bool, а bool вызвать нельзя (TypeError: 'bool' object is not callable).

Итог

Соглашения об именах

  • name — публичный атрибут; _name — внутренний, Python его не защищает.
  • __name — искажение имени в _Class__name, защита от совпадений в наследниках, а не от доступа.

property

  • @property — геттер: product.price вызывает метод.
  • @price.setter — сеттер: product.price = x вызывает метод с проверкой.
  • Значение хранится в другом атрибуте (_price), иначе бесконечная рекурсия.
  • В __init__ — self.price = price, чтобы проверка работала и при создании.

Только чтение и вычисляемые поля

  • Свойство без сеттера — только для чтения: property 'stock' of 'Product' object has no setter.
  • Вычисляемое свойство (is_available) считается при каждом чтении и пишется без скобок.

Вопросы на собеседовании

Есть ли в Python приватные атрибуты?

Настоящих — нет. _x — соглашение «не трогать снаружи». __x включает искажение имени (_Class__x), но атрибут всё равно доступен под новым именем. Его назначение — избежать случайных конфликтов имён при наследовании. Python доверяет программисту: «мы все взрослые люди».

Зачем нужен property, если можно написать методы get_price() и set_price()?

property позволяет начать с обычного атрибута и добавить проверку позже, не меняя код, который этим атрибутом пользуется: product.price остаётся product.price. Методы get_/set_ пришлось бы вписать во все места использования.

Почему в сеттере нельзя написать self.price = value?

Присваивание self.price снова вызывает сеттер этого же свойства — получается бесконечная рекурсия и RecursionError. Значение хранят в отдельном атрибуте, обычно с подчёркиванием: self._price.

Как сделать атрибут только для чтения?

Объявить property без сеттера: чтение работает, присваивание даёт AttributeError. Данные хранятся во внутреннем атрибуте и меняются только методами класса.

Глубже

Как property устроен изнутри и как написать своё «поле с проверкой» для многих атрибутов сразу — в курсе «Python под капотом», тема «Дескрипторы».

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить