Инкапсуляция и 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. Видим проблему публичного атрибута
Публичный атрибут принимает любое значение:
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 не изменилась:
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:
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. Присваивание идёт через сеттер, поэтому проверку нельзя обойти и при создании:
Product("Коврик", Decimal("-5"))
Результат:
ValueError: цена не может быть отрицательной: -5
Шаг 4. Делаем остаток только для чтения
Свойство без сеттера читается, но не присваивается. Остаток меняют только методы, где есть проверки:
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 создаёт новый атрибут:
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 под капотом», тема «Дескрипторы».