Инкапсуляция в Python
Инкапсуляция в Python: публичные, защищённые и приватные члены, искажение имён и геттеры/сеттеры через @property.
Инкапсуляция — один из четырёх столпов объектно-ориентированного программирования. Она означает объединение данных объекта (атрибутов) и методов, работающих с этими данными, в единое целое, с контролем над тем, какие части объекта доступны внешнему миру для чтения или изменения.
При правильном применении инкапсуляция поддерживает внутреннее состояние объекта согласованным, скрывает детали реализации (что позволяет изменять их позже без нарушения работы вызывающего кода) и делает классы безопаснее в использовании.
В этой главе рассматривается:
- Что такое инкапсуляция и зачем она нужна
- Публичные, защищённые и приватные члены — и соглашения об именовании, принятые в Python
- Искажение имён — как на самом деле работают атрибуты с
__двойным подчёркиванием - Геттеры и сеттеры с
@property - Реальный пример, объединяющий всё вышесказанное
Перед чтением убедитесь, что вы знакомы с классами и объектами Python. Об управлении доступом через вычисляемые атрибуты читайте в тесно связанной главе о @property.
Зачем нужна инкапсуляция
Представьте банковский счёт. Внутри он хранит баланс. Если бы баланс был обычным атрибутом, который может установить кто угодно, ничто не помешает ошибке (или злоумышленнику) написать:
account.balance = -9999999Инкапсуляция решает эту проблему, скрывая баланс за управляемым интерфейсом. Код вне класса может только вносить или снимать деньги через методы, которые соблюдают бизнес-правила. Внутреннее хранилище — это деталь реализации; вызывающий код никогда не обращается к ней напрямую.
Три преимущества, которые это даёт:
- Целостность данных — логика проверки в одном месте, применяемая каждый раз.
- Гибкость — можно изменить внутреннее представление (например, хранить баланс в центах вместо долларов), не затрагивая вызывающий код.
- Снижение связности — вызывающий код зависит только от публичного интерфейса, а не от того, как работает класс изнутри.
Уровни доступа: публичные, защищённые и приватные
В Python нет модификаторов доступа, таких как ключевые слова private или public. Вместо этого используется соглашение об именовании для обозначения намерений:
| Префикс | Пример | Уровень доступа | Значение |
|---|---|---|---|
| Без префикса | balance | Публичный | Предназначен для использования кем угодно |
Одиночное подчёркивание _ | _balance | Защищённый | Для внутреннего использования и подклассов; доступен извне, но не рекомендуется |
Двойное подчёркивание __ | __pin | Приватный | Только для этого класса; Python активно переименовывает его, чтобы затруднить внешний доступ |
Это соглашения и механизмы, а не жёсткие правила, которые проверяет компилятор. Python доверяет разработчикам соблюдать эти сигналы.
Публичные члены
Публичные атрибуты и методы образуют официальный интерфейс класса — ту часть, которую предполагается использовать вызывающим кодом:
class BankAccount:
account_type = 'savings' # public class attribute
def __init__(self, owner, balance):
self.owner = owner # public instance attribute
def deposit(self, amount):
pass # public methodСпециальное именование не требуется. Любой код может свободно читать или изменять публичный член.
Защищённые члены (одиночное подчёркивание _)
Одиночное ведущее подчёркивание — это сигнал, означающий «это внутренняя деталь — пожалуйста, не полагайтесь на неё извне класса». Python это не применяет принудительно; это чисто соглашение:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # protected — internal, but subclasses may need it
def _validate_amount(self, amount): # protected helper
return isinstance(amount, (int, float)) and amount > 0_balance по-прежнему доступен как account._balance снаружи, но подчёркивание предупреждает других разработчиков (и линтеры), что они нарушают предполагаемый контракт.
Распространённый случай применения: базовый класс хранит данные в атрибуте с _, чтобы подклассы могли его читать, скрывая при этом от несвязанного кода.
Приватные члены (двойное подчёркивание __)
Двойное ведущее подчёркивание запускает искажение имён — Python переименовывает атрибут внутри в _ИмяКласса__атрибут. Это сильно затрудняет случайный доступ извне:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
self.__pin = 1234 # private — not meant to be touched at all
def verify_pin(self, pin):
return pin == self.__pinСнаружи класса:
acc = BankAccount('Alice', 1000)
print(acc.owner) # Alice — public, fine
print(acc._balance) # 1000 — protected, works but frowned upon
print(acc.__pin) # AttributeError: 'BankAccount' object has no attribute '__pin'Атрибут всё ещё существует, но под другим именем. О том, как его найти, читайте в следующем разделе.
Искажение имён
Когда Python видит self.__name внутри определения класса, он внутренне переписывает это в self._ИмяКласса__name. Это и есть искажение имён. Цель — избежать случайных коллизий в подклассах, а не обеспечить настоящую защиту.
class Counter:
def __init__(self):
self.__count = 0
def increment(self):
self.__count += 1
def value(self):
return self.__count
c = Counter()
c.increment()
c.increment()
print(c.value()) # 2
# Direct access fails:
# print(c.__count) # AttributeError
# But mangled name still works if you know it:
print(c._Counter__count) # 2Можно просмотреть все атрибуты с помощью vars() или dir(), чтобы обнаружить искажённое имя:
print(list(vars(c)))
# ['_Counter__count']Искажение имён и наследование
Искажение имён особенно полезно при наследовании. Без него подкласс мог бы случайно перезаписать приватный атрибут родителя, используя то же имя. С искажением каждый класс получает собственное пространство имён:
class Base:
def __init__(self):
self.__secret = 'base'
def reveal(self):
return self.__secret # accesses _Base__secret
class Child(Base):
def __init__(self):
super().__init__()
self.__secret = 'child' # stored as _Child__secret, not the same thing
def reveal_child(self):
return self.__secret # accesses _Child__secret
c = Child()
print(c.reveal()) # base — Base.reveal() reads _Base__secret
print(c.reveal_child()) # child — Child.reveal_child() reads _Child__secretОба атрибута сосуществуют без коллизий, что было бы невозможно без искажения имён.
Геттеры и сеттеры с @property
Во многих языках пишут явные методы get_x() и set_x(). Python предлагает более чистый подход: декоратор @property позволяет представить метод как обычный атрибут, поэтому вызывающий код остаётся читаемым, а вы сохраняете полный контроль над чтением и записью.
Базовый геттер
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsiusВызывающий код читает t.celsius, а не t.celsius(). @property делает вызов метода невидимым:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses neededДобавление сеттера с проверкой
Совместите @property с .setter для проверки значений перед сохранением:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = 100
print(t.fahrenheit) # 212.0
t.celsius = -300 # ValueError: Temperature below absolute zerofahrenheit — это вычисляемое свойство только для чтения: сеттер не определён, поэтому Python вызывает AttributeError при попытке присвоить ему значение.
Почему @property предпочтительнее обычных геттеров/сеттеров?
Можно начать с обычного публичного атрибута и обновить его до свойства позже, не изменяя вызывающий код:
# v1 — plain attribute
class Circle:
def __init__(self, radius):
self.radius = radius
# v2 — property with validation, same public interface
class Circle:
def __init__(self, radius):
self.radius = radius # still works from the caller's point of view
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = valueКод, который писал c.radius = 5, продолжит работать без изменений. Меняется только поведение — теперь значение проверяется.
Полный справочник по @property, включая удалители, см. в Python @property.
Полный пример: пользовательский аккаунт
Следующий пример показывает совместную работу всех трёх уровней доступа в реалистичном классе:
class UserAccount:
def __init__(self, username, password):
self.username = username # public
self._login_attempts = 0 # protected — subclasses may need this
self.__password_hash = self.__hash(password) # private
def __hash(self, password):
"""Private helper — implementation detail, may change."""
return hash(password)
def check_password(self, password):
"""Public method — part of the official interface."""
return self.__hash(password) == self.__password_hash
def login(self, password):
if self._login_attempts >= 3:
return 'Account locked'
if self.check_password(password):
self._login_attempts = 0
return 'Login successful'
self._login_attempts += 1
return f'Wrong password ({self._login_attempts}/3)'
user = UserAccount('alice', 'secret123')
print(user.login('bad')) # Wrong password (1/3)
print(user.login('bad')) # Wrong password (2/3)
print(user.login('bad')) # Wrong password (3/3)
print(user.login('secret123')) # Account lockedОбратите внимание:
usernameпубличный — его может читать кто угодно._login_attemptsзащищённый — подклассThrottledAccountмог бы читать его для реализации более умной логики.__password_hashи__hash()приватные — стратегия хранения пароля полностью внутренняя. Вызывающему коду незачем её знать, и если вы позже перейдёте на bcrypt, изменить нужно будет только эти два элемента.
Инкапсуляция и другие принципы ООП
Инкапсуляция — один из четырёх принципов ООП:
| Принцип | Краткое определение |
|---|---|
| Инкапсуляция | Объединение данных и методов; скрытие внутренних деталей |
| Наследование | Позволяет классу повторно использовать и расширять другой класс |
| Полиморфизм | Позволяет разным типам реагировать на один и тот же вызов метода |
| Абстракция | Предоставляет упрощённый интерфейс; скрывает сложность |
Подробнее о других принципах читайте в главах Наследование в Python, Полиморфизм в Python и Абстрактные классы Python.
Краткий справочник
| Соглашение | Что означает | Принудительно применяется Python? |
|---|---|---|
name | Публичный — используйте свободно | Нет (всегда доступен) |
_name | Защищённый — внутреннее использование | Нет (доступен, но по соглашению не рекомендуется) |
__name | Приватный — только этот класс | Частично — имя искажается в _ИмяКласса__name |
@property | Управляемый доступ на чтение | Да — хуки геттера/сеттера/удалителя |
@name.setter | Управляемый доступ на запись с проверкой | Да |