Инкапсуляция в Python
Допустим, мы пишем класс банковского счёта:
Python 3.13class BankAccount: def __init__(self, balance): self.balance = balance account = BankAccount(1000) print(account.balance) # 1000 account.balance = -1_000_000 # упс
Класс не возражает: balance — публичный атрибут, и присвоить ему можно что угодно, включая такое, чего не должно быть никогда. Класс не гарантирует свои инварианты (правила, которые должны выполняться всегда, например «баланс не отрицательный»).
Инкапсуляция — это идея «у объекта есть публичный интерфейс (через что с ним общаются снаружи) и внутреннее состояние (внутрь не лазать)». Внешний код вызывает методы, а класс внутри сам следит, чтобы данные оставались валидными.
Соглашение об одном подчёркивании
В Python нет ключевого слова private. Вместо него используется соглашение: атрибут или метод, который начинается с подчёркивания, считается «внутренним», то есть снаружи трогать не следует:
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). Язык не запрещает — он сигналит, что трогать не стоит. Ответственность на программисте.

Свойства: атрибут снаружи, метод внутри
Часто хочется, чтобы снаружи account.balance выглядело как обычный атрибут, но при этом внутри был метод (например, с проверкой). В Java для этого пишут getBalance() и setBalance(). В 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 превращает метод в «вычисляемый атрибут». @balance.setter определяет, что произойдёт при присваивании. Снаружи всё выглядит как account.balance = 500, но внутри класса срабатывает проверка.
В примере встречаются raise и try/except — это механизм ошибок: raise прерывает операцию и сообщает, что значение недопустимо, а try/except перехватывает это в вызывающем коде. Им посвящён отдельный урок дальше в курсе; здесь достаточно понимать, что проверка в сеттере не даёт записать некорректное значение.
В Java и C++ часто пишут get_x() и set_x() методы. В Python так делать не принято: используйте @property.
Свойство только для чтения
Если у @property нет сеттера, атрибут получается доступным только для чтения. Это удобно для вычисляемых значений, которые не имеет смысла «устанавливать» извне:
Python 3.13import math class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value <= 0: raise ValueError("Радиус должен быть положительным") self._radius = value @property def area(self): return math.pi * self._radius ** 2 circle = Circle(5) print(f"Радиус: {circle.radius}, площадь: {circle.area:.2f}")Радиус: 5, площадь: 78.54# Меняем радиус, площадь пересчитывается автоматически circle.radius = 7 print(f"Радиус: {circle.radius}, площадь: {circle.area:.2f}")Радиус: 7, площадь: 153.94# area нельзя присвоить (нет сеттера) try: circle.area = 100 except AttributeError as e: print(f"Ошибка: {e}")Ошибка: property 'area' of 'Circle' object has no setter
area всегда возвращает актуальное значение, и присвоить ему ничего нельзя: «установки площади» не должно быть, она вытекает из радиуса.
А что насчёт двух подчёркиваний?
Иногда можно встретить атрибуты с двумя подчёркиваниями в начале: self.__balance. Это запускает в Python механизм name mangling: атрибут переименовывается внутри объекта в _ClassName__balance. Это нужно в редких случаях (например, чтобы атрибут точно не пересёкся с одноимённым атрибутом в дочернем классе) и в обычном коде почти не встречается. По умолчанию используйте одно подчёркивание.
Что даёт инкапсуляция
Главное — класс становится ответственным за свои данные. Снаружи невозможно (по соглашению) обойти его проверки и оставить объект в невалидном состоянии. Если потом нужно поменять способ хранения (например, _balance стал словарём с историей операций), внешний код не сломается, потому что он общается с классом через тот же интерфейс — deposit, withdraw, balance.
Проверка понимания
Какой уровень доступа наиболее подходит для атрибута, который должен быть доступен для чтения, но не должен изменяться вне класса?
В следующем уроке возьмём третий принцип ООП — полиморфизм: как через единый интерфейс работать с объектами разных классов и почему это делает код проще.
