Python

Инкапсуляция в Python: простой пример

Инкапсуляция в Python означает, что объект не раскрывает все внутренние детали наружу. Вместо прямого изменения данных вы даете понятные методы и свойства: пополнить счет, списать деньги, поменять email, добавить товар, закрыть заказ. Так код становится устойчивее, потому что правила изменения данных живут в одном месте

В Python инкапсуляция строится не на жестком запрете, как в некоторых языках, а на соглашениях и аккуратном интерфейсе класса. Одинарное подчеркивание показывает: «это внутренняя деталь, не трогайте напрямую»

Пример без инкапсуляции

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

account = BankAccount(1000)
account.balance = -500
print(account.balance)

Код работает, но смысл счета сломан. Баланс стал отрицательным без проверки, хотя в нашей модели это может быть запрещено

Метод вместо прямого изменения

class BankAccount:
    def __init__(self, balance):
        if balance < 0:
            raise ValueError("Начальный баланс не может быть отрицательным")
        self._balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма пополнения должна быть больше нуля")
        self._balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Сумма списания должна быть больше нуля")
        if amount > self._balance:
            raise ValueError("Недостаточно средств")
        self._balance -= amount

    def get_balance(self):
        return self._balance

account = BankAccount(1000)
account.deposit(500)
account.withdraw(300)
print(account.get_balance())

Теперь баланс меняется только через методы. В каждом методе есть проверка, поэтому объект защищает свое состояние от случайных неправильных значений

Свойство property для чтения

В Python часто используют @property, чтобы читать значение как атрибут, но не давать менять его напрямую

class BankAccount:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

account = BankAccount(1000)
print(account.balance)

Если не добавить setter, присваивание account.balance = 0 вызовет ошибку. Это удобно, когда значение можно читать, но менять нужно только через методы с правилами

Setter с проверкой

Иногда значение можно менять напрямую по смыслу, но только после проверки

class User:
    def __init__(self, email):
        self.email = email

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        value = value.strip().lower()
        if "@" not in value:
            raise ValueError("Некорректный email")
        self._email = value

user = User("TEST@EXAMPLE.COM")
print(user.email)

Здесь снаружи видно простое присваивание, но внутри объект нормализует строку и проверяет email

Два подчеркивания

Двойное подчеркивание включает name mangling: Python меняет имя атрибута внутри класса. Это не настоящая абсолютная защита, но способ избежать случайного конфликта имен в наследовании

class TokenStorage:
    def __init__(self, token):
        self.__token = token

    def masked(self):
        return self.__token[:4] + "..."

Для обычного учебного и прикладного кода чаще хватает одного подчеркивания и понятных методов. Двойное подчеркивание используйте тогда, когда действительно понимаете, зачем оно нужно

Где инкапсуляция полезна

  • банковский счет не должен уходить в минус без правила
  • заказ нельзя закрыть без оплаты
  • email нужно нормализовать перед сохранением
  • список товаров должен пересчитывать итоговую сумму
  • настройки нельзя менять на случайные значения

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

Первая ошибка — думать, что подчеркивание полностью запрещает доступ. В Python это соглашение, а не замок

Вторая ошибка — делать getter и setter для каждого поля без причины. Если поле обычное и не требует проверки, можно оставить его открытым

Третья ошибка — прятать данные, но не давать удобных методов. Инкапсуляция нужна не для усложнения, а для ясного интерфейса объекта

Инкапсуляция на примере корзины

Корзина должна сама пересчитывать сумму и не позволять добавить товар с нулевым количеством

class Cart:
    def __init__(self):
        self._items = []

    def add(self, title, price, quantity=1):
        if price <= 0:
            raise ValueError("Цена должна быть больше нуля")
        if quantity <= 0:
            raise ValueError("Количество должно быть больше нуля")
        self._items.append({"title": title, "price": price, "quantity": quantity})

    @property
    def total(self):
        return sum(item["price"] * item["quantity"] for item in self._items)

cart = Cart()
cart.add("Книга", 700, 2)
print(cart.total)

Снаружи не нужно знать, как именно хранится список товаров. Пользователь класса видит понятный метод add() и свойство total

По этой теме полезно отдельно посмотреть Составьте еженедельное расписание занятий, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Аннотации типов для декораторов в Python, чтобы расширить контекст и сравнить подходы

Почему не стоит отдавать внутренний список напрямую

Если вернуть _items наружу, внешний код сможет изменить список без проверок

items = cart._items
items.append({"title": "Ошибка", "price": -100, "quantity": 1})

Лучше дать метод чтения, который возвращает копию

class Cart:
    def __init__(self):
        self._items = []

    def items(self):
        return list(self._items)

Копия не защищает вложенные словари полностью, но уже снижает риск случайной порчи списка. Для более строгой модели можно использовать неизменяемые структуры или отдельный класс товара

Как понять, что инкапсуляция нужна

Инкапсуляция нужна, когда у данных есть правила. Если значение можно менять как угодно, простое поле нормально. Если перед изменением нужно проверить сумму, статус, права, формат или взаимосвязь полей, лучше спрятать прямое изменение за методом

Признак хорошего класса: внешний код говорит на языке задачи. Не account._balance -= 100, а account.withdraw(100). Не order.status = "closed", а order.close()

Инкапсуляция и тесты

Класс с понятными методами легче тестировать. Проверяйте не внутреннюю переменную, а публичное поведение

account = BankAccount(1000)
account.deposit(500)
account.withdraw(200)

assert account.balance == 1300

Если тест лезет в _balance, он привязывается к внутреннему устройству класса. Завтра вы можете хранить баланс иначе, а внешний смысл объекта останется тем же. Хороший тест должен пережить такую внутреннюю замену

Пример плохого интерфейса

order.status = "paid"
order.paid_at = "2026-07-02"
order.can_ship = True

Внешний код сам собирает состояние заказа из разных полей. Легко забыть одно из них и получить странную ошибку

Лучше так

order.mark_as_paid(payment_id="pay_123")

Метод внутри сам меняет статус, дату, платеж и доступность отправки. Это и есть практический смысл инкапсуляции: правила изменения состояния находятся рядом с данными

Нормальная мера строгости

Не нужно превращать каждый класс в крепость. В Python принято доверять разработчику, но давать понятный интерфейс. Одинарное подчеркивание, методы с проверками и @property покрывают большинство задач новичка и небольшого проекта

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x