W3docs

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

Инкапсуляция в Python: публичные, защищённые и приватные члены, искажение имён и геттеры/сеттеры через @property.

Инкапсуляция — один из четырёх столпов объектно-ориентированного программирования. Она означает объединение данных объекта (атрибутов) и методов, работающих с этими данными, в единое целое, с контролем над тем, какие части объекта доступны внешнему миру для чтения или изменения.

При правильном применении инкапсуляция поддерживает внутреннее состояние объекта согласованным, скрывает детали реализации (что позволяет изменять их позже без нарушения работы вызывающего кода) и делает классы безопаснее в использовании.

В этой главе рассматривается:

  • Что такое инкапсуляция и зачем она нужна
  • Публичные, защищённые и приватные члены — и соглашения об именовании, принятые в Python
  • Искажение имён — как на самом деле работают атрибуты с __двойным подчёркиванием
  • Геттеры и сеттеры с @property
  • Реальный пример, объединяющий всё вышесказанное

Перед чтением убедитесь, что вы знакомы с классами и объектами Python. Об управлении доступом через вычисляемые атрибуты читайте в тесно связанной главе о @property.

Зачем нужна инкапсуляция

Представьте банковский счёт. Внутри он хранит баланс. Если бы баланс был обычным атрибутом, который может установить кто угодно, ничто не помешает ошибке (или злоумышленнику) написать:

account.balance = -9999999

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

Три преимущества, которые это даёт:

  1. Целостность данных — логика проверки в одном месте, применяемая каждый раз.
  2. Гибкость — можно изменить внутреннее представление (например, хранить баланс в центах вместо долларов), не затрагивая вызывающий код.
  3. Снижение связности — вызывающий код зависит только от публичного интерфейса, а не от того, как работает класс изнутри.

Уровни доступа: публичные, защищённые и приватные

В 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 + 32
t = 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 zero

fahrenheit — это вычисляемое свойство только для чтения: сеттер не определён, поэтому 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Управляемый доступ на запись с проверкойДа

Практика

Практика
Что означает одиночное ведущее подчёркивание (например, `_balance`) в Python?
Что означает одиночное ведущее подчёркивание (например, `_balance`) в Python?
Was this page helpful?