Инкапсуляция в Python
Заведём класс-копилку: deposit пополняет баланс и не пропускает отрицательную сумму. Проверка на месте — но обойти её проще простого:
Python 3.13class BankAccount: def __init__(self, balance): self.balance = balance def deposit(self, amount): if amount > 0: self.balance += amount account = BankAccount(1000) account.deposit(-500) # проверка не пропустит print(account.balance)1000account.balance = -1_000_000 # а так — пожалуйста print(account.balance)-1000000
Проверка в deposit никуда не делась — мы её просто обошли: balance — обычный атрибут, и присвоить ему можно что угодно. Класс не может гарантировать свои правила — например, «баланс не бывает отрицательным».
Инкапсуляция — ответ на эту проблему: у объекта есть публичный интерфейс (то, через что с ним общаются снаружи) и внутреннее состояние (внутрь не лазать). Внешний код вызывает методы, а класс внутри сам следит, чтобы данные оставались корректными.
Соглашение об одном подчёркивании
Как отличить внутреннее от публичного? В Python для этого есть соглашение: атрибут или метод, чьё имя начинается с подчёркивания, считается «внутренним» — снаружи его трогать не следует:
Python 3.13class BankAccount: def __init__(self, balance): self._balance = balance # подчёркивание = «внутреннее» def get_balance(self): return self._balance def deposit(self, amount): if amount > 0: self._balance += amount def withdraw(self, amount): if 0 < amount <= self._balance: self._balance -= amount account = BankAccount(1000) account.deposit(500) account.withdraw(2000) # запрошено больше баланса, игнорируется print(account.get_balance())1500
Снаружи доступны только deposit, withdraw и get_balance: всё, что нужно для работы со счётом. _balance менять снаружи технически можно (Python не запрещает), но соглашение говорит: не надо, иначе вы обходите проверки класса.
Эту философию в Python-сообществе формулируют так: «мы все взрослые здесь» (we're all consenting adults here). Язык не запрещает — он сигналит, что трогать не стоит. Ответственность на программисте.
_balance класс меняет только самСвойства: атрибут снаружи, метод внутри
Часто хочется, чтобы снаружи account.balance выглядело как обычный атрибут, но при этом внутри был метод с проверкой. Для этого в Python есть @property.
Строка с @ над методом — это декоратор: приём, который меняет поведение метода. Декораторам посвящён отдельный урок дальше в курсе; пока достаточно знать, что делают эти два:
Python 3.13class Account: def __init__(self, balance): self._balance = balance @property def balance(self): return self._balance @balance.setter def balance(self, value): if value < 0: raise ValueError("Баланс не может быть отрицательным") self._balance = value account = Account(1000) # Используется как обычный атрибут: print(account.balance)1000account.balance = 500 # вызовет setter с проверкой print(account.balance)500# Попытка установить отрицательное значение: try: account.balance = -100 except ValueError as e: print(f"Ошибка: {e}")Ошибка: Баланс не может быть отрицательным
@property превращает метод в «вычисляемый атрибут»: снаружи account.balance читается без скобок. @balance.setter определяет, что произойдёт при присваивании: снаружи всё выглядит как обычное присваивание, но внутри класса срабатывает проверка.
В примере встречаются raise и try/except — это механизм ошибок: raise прерывает операцию и сообщает, что значение недопустимо, а try/except перехватывает это в вызывающем коде. Им посвящён отдельный урок дальше в курсе; здесь достаточно понимать, что проверка в сеттере не даёт записать некорректное значение.
Свойство только для чтения
Если у @property нет сеттера, атрибут получается доступным только для чтения. Это удобно для вычисляемых значений, которые не имеет смысла «устанавливать» извне:
Python 3.13import math class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return math.pi * self.radius ** 2 circle = Circle(5) print(f"Радиус: {circle.radius}, площадь: {round(circle.area, 2)}")Радиус: 5, площадь: 78.54# area нельзя присвоить (нет сеттера) try: circle.area = 100 except AttributeError as e: print(f"Ошибка: {e}")Ошибка: property 'area' of 'Circle' object has no setter
area всегда возвращает актуальное значение, а присвоить ему ничего нельзя — и правильно: площадь не хранится отдельно, она вытекает из радиуса.
Что даёт инкапсуляция
Главное — класс становится ответственным за свои данные. Снаружи невозможно (по соглашению) обойти его проверки и оставить объект с некорректными данными. Если потом нужно поменять способ хранения (например, _balance стал словарём с историей операций), внешний код не сломается, потому что он общается с классом через тот же публичный интерфейс — deposit, withdraw, balance.
Проверка понимания
Как оформить атрибут, который снаружи можно читать, но нельзя менять?
В следующем уроке возьмём третий принцип ООП — полиморфизм: как через единый интерфейс работать с объектами разных классов и почему это делает код проще.
