Инкапсуляция в Python

Заведём класс-копилку: deposit пополняет баланс и не пропускает отрицательную сумму. Проверка на месте — но обойти её проще простого:

Python 3.13
class 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)
1000
account.balance = -1_000_000   # а так — пожалуйста
print(account.balance)
-1000000

Проверка в deposit никуда не делась — мы её просто обошли: balance — обычный атрибут, и присвоить ему можно что угодно. Класс не может гарантировать свои правила — например, «баланс не бывает отрицательным».

Инкапсуляция — ответ на эту проблему: у объекта есть публичный интерфейс (то, через что с ним общаются снаружи) и внутреннее состояние (внутрь не лазать). Внешний код вызывает методы, а класс внутри сам следит, чтобы данные оставались корректными.

Соглашение об одном подчёркивании

Как отличить внутреннее от публичного? В Python для этого есть соглашение: атрибут или метод, чьё имя начинается с подчёркивания, считается «внутренним» — снаружи его трогать не следует:

Python 3.13
class 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). Язык не запрещает — он сигналит, что трогать не стоит. Ответственность на программисте.

внешний код
через методы
напрямую
class BankAccount
deposit()withdraw()get_balance()
внутреннее состояние_balance
Со счётом работают через методы, а _balance класс меняет только сам

Свойства: атрибут снаружи, метод внутри

Часто хочется, чтобы снаружи account.balance выглядело как обычный атрибут, но при этом внутри был метод с проверкой. Для этого в Python есть @property.

Строка с @ над методом — это декоратор: приём, который меняет поведение метода. Декораторам посвящён отдельный урок дальше в курсе; пока достаточно знать, что делают эти два:

Python 3.13
class 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)
1000
account.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.13
import 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.

Проверка понимания

Как оформить атрибут, который снаружи можно читать, но нельзя менять?

В следующем уроке возьмём третий принцип ООП — полиморфизм: как через единый интерфейс работать с объектами разных классов и почему это делает код проще.