Public, protected, private атрибуты
Мы умеем создавать объекты, прописывать атрибуты и методы, выстраивать наследование. Теперь разбираем следующий фундаментальный принцип ООП — инкапсуляцию. Узнаем, как в Python разграничиваются «публичные» и «скрытые» части объекта, и откроем один из самых необычных механизмов языка — name mangling.
🔒 Что такое инкапсуляция и зачем она нужна
Слово «инкапсуляция» происходит от латинского 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 вызовет нужный метод сам.