$ sudo teach IT
МОДУЛЬ 5 · УРОК 1

Public, protected, private атрибуты

Мы умеем создавать объекты, прописывать атрибуты и методы, выстраивать наследование. Теперь разбираем следующий фундаментальный принцип ООП — инкапсуляцию. Узнаем, как в Python разграничиваются «публичные» и «скрытые» части объекта, и откроем один из самых необычных механизмов языка — name mangling.

⏱ ~35 минут 🎓 Для новичков 🐍 Python 3

🔒 Что такое инкапсуляция и зачем она нужна

Слово «инкапсуляция» происходит от латинского capsula — «маленькая коробочка». Суть проста: мы упаковываем данные и методы внутри объекта и контролируем, что из этого доступно снаружи, а что — только для внутреннего использования.

💡 Аналогия с автомобилем. Когда вы садитесь за руль, у вас есть руль, педали и коробка передач — это публичный интерфейс, то, чем вы пользуетесь. А двигатель, топливная система, электроника — всё это внутри капота. Вам не нужно знать, как именно они работают, чтобы ехать. Более того, если бы все провода торчали наружу — вы бы случайно что-нибудь сломали. Инкапсуляция защищает внутренности объекта от случайного вмешательства снаружи.

Представьте класс BankAccount — банковский счёт. У него есть баланс. Что будет, если баланс полностью открыт, и любой внешний код может его изменить напрямую?

# Без инкапсуляции — опасно!
account = BankAccount()
account.balance = 1000000  # Кто угодно может просто присвоить любое значение!
account.balance = -999      # Или сделать баланс отрицательным — это даже не ошибка

Инкапсуляция решает эту проблему: мы скрываем баланс от прямого доступа и даём работать с ним только через специальные методы — deposit() (пополнить) и withdraw() (снять). Методы проверяют корректность операции. Никакой «магии» — просто дисциплина доступа.

🏷️ Три уровня доступа: public, protected, private

В языках вроде Java или C++ есть специальные ключевые слова: public, protected, private. Компилятор жёстко запрещает обращаться к private-атрибуту извне — нарушение правил просто не скомпилируется.

Python — другой язык. Здесь нет таких ключевых слов. Вместо них — соглашения об именовании: вы даёте атрибуту специальное имя, и все разработчики понимают, как с ним обращаться. Давайте разберём эти три соглашения.

🟢

Public

Обычное имя

name, age, title

Доступен везде — и внутри класса, и снаружи. Это основная часть интерфейса объекта.

🟡

Protected

Одно подчёркивание

_balance, _data

Технически доступен извне, но имя говорит: «не трогай». Для внутреннего использования и дочерних классов.

🔴

Private

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

__secret, __pin

Python применяет name mangling — реально меняет имя атрибута. Прямой доступ снаружи вызывает ошибку.

Посмотрим сразу на живой пример — все три вида атрибутов в одном классе:

💻 Пример 1: три вида атрибутов в одном классе

Создадим класс Person — человек. У него есть имя (публичное — любой может узнать), внутренний рейтинг доверия (защищённый — для внутреннего использования), и секретный PIN-код (приватный — никто снаружи не должен видеть).

class Person:
    def __init__(self, name, trust_score, pin):
        # Public: доступен откуда угодно
        self.name = name

        # Protected: одно подчёркивание — соглашение "не трогай извне"
        self._trust_score = trust_score

        # Private: два подчёркивания — Python применит name mangling
        self.__pin = pin

    def get_info(self):
        # Внутри класса доступны все три
        print(f"Имя: {self.name}")
        print(f"Рейтинг доверия: {self._trust_score}")
        print(f"PIN: {self.__pin}")   # Внутри класса — работает!


person = Person("Алиса", 95, 1234)

# Доступ к public — без проблем
print(person.name)          # Алиса

# Доступ к protected — технически работает, но "не следует"
print(person._trust_score)  # 95

# Доступ к __pin напрямую — AttributeError!
print(person.__pin)         # AttributeError: 'Person' object has no attribute '__pin'

🚨 Важно: person.__pin вызывает AttributeError — Python говорит, что такого атрибута нет. Но атрибут существует! Просто под другим именем. Сейчас разберём, что произошло.

Метод get_info() внутри класса работает со всеми тремя атрибутами без проблем. Ограничение касается только внешнего доступа — кода вне тела класса.

🔧 Name mangling — что Python делает с двойным подчёркиванием

Когда вы пишете атрибут с двумя подчёркиваниями в начале — например, __pin — Python делает с ним кое-что интересное. Он автоматически переименовывает этот атрибут по схеме:

Схема name mangling:

__attr  →  _ИмяКласса__attr

Например: __pin в классе Person становится _Person__pin

Именно поэтому person.__pin даёт ошибку — атрибута с таким именем просто нет! Но если знать настоящее имя — доступ сработает. Давайте убедимся в этом:

class Person:
    def __init__(self, name, pin):
        self.name = name
        self.__pin = pin   # Python переименует это в _Person__pin


person = Person("Алиса", 1234)

# Напрямую — AttributeError
# print(person.__pin)   # AttributeError!

# Через "правильное" имя — работает!
print(person._Person__pin)   # 1234

# Убеждаемся сами: смотрим на __dict__ объекта
print(person.__dict__)
# {'name': 'Алиса', '_Person__pin': 1234}
# Видите? Атрибут хранится под именем _Person__pin, а не __pin!

💡 Зачем такой трюк? Главная цель name mangling — не безопасность, а защита от случайного переопределения в дочерних классах. Если дочерний класс объявит атрибут с тем же именем __pin, он получит имя _ChildClass__pin — и не затрёт атрибут родителя _Person__pin. Пространства имён не конфликтуют.

Слово mangling по-английски означает «искажение», «коверкание». Python буквально искажает имя атрибута, добавляя к нему префикс с именем класса. После такого «искажения» случайно обратиться к нему снаружи почти невозможно — только если знаешь настоящее имя.

🔍 Смотрим «под капот» через __dict__

У каждого объекта в Python есть специальный атрибут __dict__ — это обычный словарь, в котором хранятся все атрибуты объекта с их настоящими именами. Это отличный инструмент, чтобы увидеть, что происходит на самом деле.

class BankAccount:
    def __init__(self, owner, balance, pin):
        self.owner = owner          # public
        self._balance = balance     # protected
        self.__pin = pin            # private — будет переименован!


account = BankAccount("Борис", 50000, 9999)

# Заглядываем внутрь объекта
print(account.__dict__)
# {'owner': 'Борис', '_balance': 50000, '_BankAccount__pin': 9999}

# Обратите внимание:
# - owner        → хранится как owner (без изменений)
# - _balance     → хранится как _balance (без изменений)
# - __pin        → хранится как _BankAccount__pin (name mangling!)

🎯 Вот оно! В словаре __dict__ чётко видно: атрибут __pin превратился в _BankAccount__pin. Это и есть name mangling в действии. Одиночное подчёркивание у _balance Python не трогает — оно остаётся как есть.

__dict__ — очень полезный инструмент при отладке. Когда что-то идёт не так с атрибутами, просто напечатайте print(obj.__dict__) и увидите всё, что внутри объекта.

🛡️ Protected на практике: соглашение, а не запрет

Давайте разберём ситуацию с protected подробнее. Одиночное подчёркивание — это чисто договорённость между разработчиками. Python никак не ограничивает доступ к таким атрибутам технически.

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

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            print(f"Пополнено на {amount}. Баланс: {self._balance}")

    def get_balance(self):
        return self._balance


account = BankAccount("Алиса", 1000)

# Это работает — Python не запрещает
print(account._balance)   # 1000

# Но это считается "плохим тоном"
account._balance = 999999   # Технически можно, но НЕ следует!

# Правильный способ — через методы интерфейса
account.deposit(500)         # Пополнено на 500. Баланс: 1000500
print(account.get_balance()) # 1000500

⚠️ Правило: Когда вы видите атрибут с одним подчёркиванием — воспринимайте его как знак «Служебный вход. Посторонним вход воспрещён». Вы физически можете туда войти, но это не ваша территория. Используйте официальный API класса — его публичные методы.

Protected атрибуты нужны в первую очередь для дочерних классов. Когда вы пишете класс-наследник, вы «свой» по отношению к родителю, и вам разрешено обращаться к _protected атрибутам:

class SavingsAccount(BankAccount):
    def __init__(self, owner, balance, interest_rate):
        super().__init__(owner, balance)
        self._interest_rate = interest_rate   # свой protected атрибут

    def apply_interest(self):
        # Дочерний класс обращается к _balance родителя — это нормально
        interest = self._balance * self._interest_rate / 100
        self._balance += interest
        print(f"Начислены проценты: {interest:.2f}. Новый баланс: {self._balance:.2f}")


savings = SavingsAccount("Боб", 10000, 5)
savings.apply_interest()   # Начислены проценты: 500.00. Новый баланс: 10500.00

Видите? Внутри дочернего класса SavingsAccount мы обращаемся к self._balance — это нормально и считается правильной практикой. «Protected» — значит «для меня и для тех, кто меня наследует».

🤝 «Мы все взрослые люди» — философия Python

В сообществе Python есть знаменитая фраза: «We're all consenting adults here» — «Мы все здесь взрослые по доброй воле». Это отражает подход создателей языка к инкапсуляции.

🧑‍💻 Философия Python: мы не считаем разработчиков безответственными детьми, которых нужно насильно ограждать от внутренних деталей. Мы доверяем вам знать, что делаете. Если видите _balance — вы взрослый человек и понимаете, что лезть туда не стоит. Если всё-таки лезете — это ваша ответственность.

Именно поэтому в Python нет жёсткого enforcement как в Java. Это сознательное решение: соглашения по именованию + доверие к разработчику — вместо принудительных ограничений компилятора.

При этом double-underscore и name mangling существуют не для «настоящей приватности», а для практической цели — избежать конфликтов имён при наследовании. Это стоит помнить.

✅ Делайте так

  • Используйте публичный API
  • Соблюдайте соглашения по именованию
  • Пишите методы-геттеры/сеттеры
  • Уважайте одиночное подчёркивание

❌ Избегайте этого

  • Обращаться к _protected извне
  • Использовать _Class__attr напрямую
  • Изменять атрибуты в обход методов
  • «Взламывать» чужой приватный код

🧩 Полный пример: правильная инкапсуляция

Соберём всё вместе. Вот класс BankAccount, который правильно использует все три уровня доступа:

class BankAccount:
    def __init__(self, owner, balance, pin):
        # Public: имя владельца — открытая информация
        self.owner = owner

        # Protected: баланс — для внутреннего использования и наследников
        self._balance = balance

        # Private: PIN — строго внутри класса, name mangling защищает от конфликтов
        self.__pin = pin

    def deposit(self, amount):
        """Пополнить счёт — публичный метод."""
        if amount <= 0:
            print("Сумма должна быть положительной")
            return
        self._balance += amount
        print(f"Пополнено на {amount} руб. Баланс: {self._balance} руб.")

    def withdraw(self, amount, pin):
        """Снять деньги — публичный метод, требует PIN."""
        if pin != self.__pin:          # Используем __pin внутри класса — OK!
            print("Неверный PIN!")
            return
        if amount > self._balance:
            print("Недостаточно средств")
            return
        self._balance -= amount
        print(f"Снято {amount} руб. Остаток: {self._balance} руб.")

    def get_balance(self):
        """Публичный геттер для баланса."""
        return self._balance

    def __repr__(self):
        return f"BankAccount(owner={self.owner!r}, balance={self._balance})"


account = BankAccount("Алиса", 10000, 4242)

# Правильный способ работы с объектом
account.deposit(5000)          # Пополнено на 5000 руб. Баланс: 15000 руб.
account.withdraw(3000, 4242)   # Снято 3000 руб. Остаток: 12000 руб.
account.withdraw(1000, 9999)   # Неверный PIN!

print(account.owner)           # Алиса         — public, всё ок
print(account.get_balance())   # 12000          — через геттер

# Что хранится внутри объекта?
print(account.__dict__)
# {'owner': 'Алиса', '_balance': 12000, '_BankAccount__pin': 4242}
# __pin стал _BankAccount__pin — name mangling!

✅ Это правильная инкапсуляция: снаружи видны только owner, deposit(), withdraw(), get_balance(). Пользователь класса работает только с этим интерфейсом. Внутренние детали — _balance и __pin — скрыты за соглашениями.

✅ Итоги урока

  • Инкапсуляция — сокрытие внутренних деталей объекта и предоставление публичного интерфейса для работы с ним.
  • Public (name) — атрибут без подчёркивания. Доступен везде, часть публичного API.
  • Protected (_balance) — одно подчёркивание. Соглашение «не трогай извне». Технически доступен, но это моветон.
  • Private (__pin) — два подчёркивания. Python применяет name mangling: __pin → _ClassName__pin.
  • Name mangling — автоматическое переименование атрибута с добавлением имени класса в качестве префикса. Цель: избежать конфликтов имён при наследовании.
  • obj.__dict__ показывает все атрибуты объекта с настоящими именами — отличный инструмент для отладки.
  • Python доверяет разработчику: «Мы все взрослые люди». Соглашения важнее принудительных ограничений.

👉 В следующем уроке мы познакомимся со свойствами — @property. Это элегантный способ сделать атрибуты «управляемыми»: добавить логику проверки при чтении и записи значения, не меняя внешний интерфейс класса. Никаких некрасивых get_balance() — просто account.balance, а Python вызовет нужный метод сам.