← Назад к вопросам

Как скрыть методы и переменные класса в Python?

1.3 Junior🔥 161 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Это один из столпов объектно-ориентированного программирования. Python имеет несколько подходов к инкапсуляции, от конвенций до механизмов языка.

Уровни видимости в Python

Потому что Python руководствуется принципом "мы все взрослые люди", он не имеет жёстких ограничений видимости как Java или C++. Вместо этого используются соглашения:

1. Публичные атрибуты (public)

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner          # Публичный атрибут
        self.balance = balance       # Публичный атрибут
    
    def deposit(self, amount):
        """Публичный метод"""
        self.balance += amount
    
    def withdraw(self, amount):
        """Публичный метод"""
        if amount <= self.balance:
            self.balance -= amount

account = BankAccount("Alice", 1000)
print(account.balance)      # 1000 - свободный доступ
account.balance = 5000000   # Можем просто изменить! (плохо)
print(account.balance)      # 5000000

Проблема: ничто не мешает нарушить инвариант класса.

2. Protected атрибуты (защищённые с одним подчёркиванием)

Конвенция, которая говорит: "Используй это на свой риск".

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance      # Protected: одно подчёркивание
    
    def _validate_balance(self, amount):  # Protected метод
        """Внутренняя проверка (не вызывай напрямую)"""
        return amount > 0
    
    def deposit(self, amount):
        """Публичный метод"""
        if self._validate_balance(amount):
            self._balance += amount
        else:
            raise ValueError("Сумма должна быть положительной")

account = BankAccount("Alice", 1000)
print(account._balance)      # 1000 - можно, но не рекомендуется
account._balance = -5000     # Технически можно, но нарушаем инвариант

Соглашение: одно подчёркивание сигнализирует: "Это внутренний API, не используй".

3. Private атрибуты (приватные с двойным подчёркиванием)

Name mangling — механизм "скрывания" с двойным подчёркиванием.

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance      # Private: двойное подчёркивание
    
    def __validate_balance(self, amount):  # Private метод
        return amount > 0
    
    def deposit(self, amount):
        if self.__validate_balance(amount):
            self.__balance += amount
    
    def get_balance(self):
        return self.__balance

account = BankAccount("Alice", 1000)
print(account.get_balance())  # 1000 - через getter
print(account.__balance)      # AttributeError!

# Но Python переименовывает атрибут (name mangling)
print(account._BankAccount__balance)  # 1000 - можно найти
account._BankAccount__balance = -5000  # Можно изменить (но не нужно)

Как работает name mangling:

class MyClass:
    def __init__(self):
        self.__private = 42

obj = MyClass()
print(dir(obj))  # Видим '_MyClass__private' вместо '__private'

# Python переименовывает __private в _ClassName__private
# Это НЕ криптографическое скрытие, а просто соглашение

Правильная инкапсуляция с @property

Управление доступом

class BankAccount:
    def __init__(self, owner, balance):
        self._owner = owner
        self._balance = balance
    
    @property
    def balance(self):
        """Getter для баланса (читаем как атрибут)"""
        return self._balance
    
    @balance.setter
    def balance(self, amount):
        """Setter с валидацией"""
        if amount < 0:
            raise ValueError("Баланс не может быть отрицательным")
        self._balance = amount
    
    @property
    def owner(self):
        return self._owner
    
    @owner.setter
    def owner(self, name):
        if not name or len(name) < 2:
            raise ValueError("Некорректное имя")
        self._owner = name

# Использование как обычные атрибуты
account = BankAccount("Alice", 1000)
print(account.balance)   # 1000 - читаем
account.balance = 2000   # 2000 - пишем с проверкой
account.balance = -100   # ValueError: Баланс не может быть отрицательным

Преимущества:

  • API остаётся простой (выглядит как атрибут)
  • Можем добавить валидацию позже
  • Не нужно менять код у клиентов

Только чтение (read-only)

class User:
    def __init__(self, user_id, name):
        self._id = user_id      # Только внутри
        self._name = name
    
    @property
    def id(self):
        """Читаем ID, но не можем изменить"""
        return self._id
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        self._name = value

user = User(123, "Alice")
print(user.id)      # 123
user.id = 456       # AttributeError: can't set attribute
user.name = "Bob"   # OK

Практические паттерны

Паттерн 1: Lazy initialization

class DataProcessor:
    def __init__(self):
        self._data = None  # Ленивая инициализация
    
    @property
    def data(self):
        if self._data is None:
            self._data = self._load_data()  # Загружаем при первом доступе
        return self._data
    
    def _load_data(self):
        """Дорогостоящая операция"""
        return [1, 2, 3, 4, 5]

processor = DataProcessor()
print(processor.data)  # Загружается здесь
print(processor.data)  # Используется кэш

Паттерн 2: Computed property

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Вычисляется при доступе"""
        return self.width * self.height
    
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(5, 3)
print(rect.area)       # 15 - вычисляется
print(rect.perimeter)  # 16 - вычисляется

rect.width = 10
print(rect.area)       # 30 - пересчитывается

Паттерн 3: Наблюдатель (Observer)

class User:
    def __init__(self, name):
        self._name = name
        self._watchers = []  # Список наблюдателей
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        old_value = self._name
        self._name = value
        # Уведомляем наблюдателей об изменении
        for watcher in self._watchers:
            watcher(f"Name changed from {old_value} to {value}")
    
    def watch(self, callback):
        """Добавляем наблюдателя"""
        self._watchers.append(callback)

user = User("Alice")
user.watch(lambda msg: print(f"[Event] {msg}"))
user.name = "Bob"  # [Event] Name changed from Alice to Bob

Защита класса целиком

Использование slots

Ограничиваем атрибуты класса для экономии памяти и защиты:

class Point:
    __slots__ = ['_x', '_y']  # Только эти атрибуты разрешены
    
    def __init__(self, x, y):
        self._x = x
        self._y = y
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y

point = Point(3, 4)
print(point.x)  # 3
point.z = 5     # AttributeError: 'Point' object has no attribute 'z'

# Преимущества:
# - Экономия памяти (~40%)
# - Предотвращение случайного добавления атрибутов
# - Быстрее доступ к атрибутам

Использование getattr и setattr

class RestrictedClass:
    _allowed = {'name', 'age'}
    
    def __setattr__(self, name, value):
        if name not in self._allowed:
            raise AttributeError(f"{name} не разрешён")
        super().__setattr__(name, value)
    
    def __getattr__(self, name):
        if name not in self._allowed:
            raise AttributeError(f"{name} не существует")
        return super().__getattribute__(name)

obj = RestrictedClass()
obj.name = "Alice"  # OK
obj.age = 25        # OK
obj.email = "test"  # AttributeError

Сравнение подходов

# 1. Публичный (public)
self.value = 10      # Полный доступ, риск нарушения инварианта

# 2. Protected
self._value = 10     # Соглашение: "используй на свой риск"

# 3. Private
self.__value = 10    # Name mangling (не полная защита)

# 4. Property (РЕКОМЕНДУЕТСЯ)
@property
def value(self):
    return self._value

@value.setter
def value(self, v):
    if v < 0:
        raise ValueError()
    self._value = v

# 5. __slots__
__slots__ = ['_value']  # Ограничение атрибутов

Лучшие практики

class GoodClass:
    """Правильная инкапсуляция"""
    
    __slots__ = ['_data', '_cache']
    
    def __init__(self, data):
        self._data = data       # Protected для внутреннего использования
        self._cache = None      # Private для кэша
    
    @property
    def data(self):
        """Публичное свойство (read-only)"""
        return self._data
    
    @property
    def cached_result(self):
        """Ленивое вычисление"""
        if self._cache is None:
            self._cache = self._expensive_computation()
        return self._cache
    
    def _expensive_computation(self):
        """Protected метод: часть внутреннего API"""
        return sum(self._data) * 2
    
    def reset_cache(self):
        """Публичный метод для управления"""
        self._cache = None

# Использование
obj = GoodClass([1, 2, 3])
print(obj.data)              # [1, 2, 3]
print(obj.cached_result)     # 12 (вычисляется)
print(obj.cached_result)     # 12 (из кэша)
obj.reset_cache()
print(obj.cached_result)     # 12 (пересчитывается)

Резюме

  • Публичные (name) — полный доступ
  • Protected (_name) — соглашение "не трогай"
  • Private (__name) — name mangling (не полная защита)
  • @property — управление доступом с валидацией (РЕКОМЕНДУЕТСЯ)
  • slots — экономия памяти и защита от случайных атрибутов
  • Python не имеет полной инкапсуляции — используй соглашения и @property
  • Лучший подход: _protected переменные + @property для публичного API
Как скрыть методы и переменные класса в Python? | PrepBro