💻 Примеры: инкапсуляция в действии

⚡ Кратко

Сначала запустите ключевые примеры без изменений, затем поменяйте входные данные и объясните получившийся результат.

Пример 1. Публичные, защищённые и приватные атрибуты

class Book:
    def __init__(self, title: str, author: str) -> None:
        self.title = title           # публичный
        self._author = author        # защищённый
        self.__notes = []            # приватный

    def add_note(self, note: str) -> None:
        self.__notes.append(note)

    def show_notes(self) -> None:
        for note in self.__notes:
            print(f"- {note}")


book = Book("1984", "George Orwell")
print(book.title)          # 1984
book.title = "Animal Farm"
print(book._author)        # доступ возможен, но нежелателен
book.add_note("Первая глава прочитана")
book.show_notes()
# print(book.__notes)      # AttributeError

Приватный список __notes доступен только через методы класса.

Пример 2. Защищённый метод для внутренней логики

class Report:
    def __init__(self, data: list[str]) -> None:
        self.data = data

    def generate(self) -> None:
        cleaned = self._prepare_data()
        print("Отчёт:")
        print("\n".join(cleaned))

    def _prepare_data(self) -> list[str]:
        return [line.strip() for line in self.data if line.strip()]


class SalesReport(Report):
    def _prepare_data(self) -> list[str]:
        raw = super()._prepare_data()
        return [line for line in raw if not line.startswith("#")]


data = [
    "  Продажи за январь  ",
    "",
    " # внутренний комментарий ",
    "  Доход: $5000 "
]
report = SalesReport(data)
report.generate()

Метод _prepare_data — защищённый. Его можно переопределить в наследнике, но внешний код пользуется только generate().

Пример 3. Геттеры и сеттеры

class Temperature:
    def __init__(self, value: float) -> None:
        self.__validate_celsius(value)
        self.__celsius = value

    def get_celsius(self) -> float:
        return self.__celsius

    def set_celsius(self, value: float) -> None:
        self.__validate_celsius(value)
        self.__celsius = value

    @staticmethod
    def __validate_celsius(value: float) -> None:
        if value < -273.15:
            raise ValueError("Температура не может быть ниже абсолютного нуля")


temp = Temperature(20)
print(temp.get_celsius())   # 20
temp.set_celsius(-300)      # ValueError

Валидация вынесена в приватный статический метод, чтобы не дублировать код.

Пример 4. Свойства через @property

class SecureUSB:
    def __init__(self, data: str, password: str) -> None:
        self.__data = data
        self.__password = password
        self.__locked = True

    def unlock(self, password: str) -> bool:
        if password == self.__password:
            self.__locked = False
            return True
        return False

    def lock(self) -> None:
        self.__locked = True

    @property
    def data(self) -> str:
        if self.__locked:
            raise PermissionError("Access denied")
        return self.__data


usb = SecureUSB("Secret plans", "1234")
print(usb.unlock("wrong"))   # False
try:
    print(usb.data)
except PermissionError as e:
    print(e)                 # Access denied

usb.unlock("1234")
print(f"Data: {usb.data}")   # Data: Secret plans
usb.lock()

Свойство data выглядит как атрибут, но внутри выполняет проверку доступа.

Пример 5. Read-only свойство

class Circle:
    def __init__(self, radius: float) -> None:
        if radius <= 0:
            raise ValueError("Радиус должен быть положительным")
        self.__radius = radius

    @property
    def radius(self) -> float:
        return self.__radius

    @property
    def area(self) -> float:
        return 3.14159 * self.__radius ** 2


c = Circle(5)
print(c.radius)   # 5
print(c.area)     # 78.53975
# c.radius = 10   # AttributeError

Пример 6. Проверка срабатывает уже при создании объекта

Разница между «сеттер есть» и «сеттер работает» — в том, как значение попадает в объект впервые.

class PersonUnsafe:
    def __init__(self, age: int) -> None:
        self.__age = age  # проверка пропущена

    @property
    def age(self) -> int:
        return self.__age

    @age.setter
    def age(self, value: int) -> None:
        if value < 0:
            raise ValueError("Возраст не может быть отрицательным")
        self.__age = value


class PersonSafe:
    def __init__(self, age: int) -> None:
        self.age = age  # значение идёт через сеттер

    @property
    def age(self) -> int:
        return self.__age

    @age.setter
    def age(self, value: int) -> None:
        if value < 0:
            raise ValueError("Возраст не может быть отрицательным")
        self.__age = value


bad = PersonUnsafe(-10)
print(bad.age)        # -10 — объект с некорректными данными

# PersonSafe(-10)     # ValueError: Возраст не может быть отрицательным

Пример 7. Универсальный дескриптор-валидатор

Одна проверка описана один раз и обслуживает сразу два поля.

class PositiveNumber:
    def __set_name__(self, owner, name: str) -> None:
        self.private_name = "_" + name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.private_name)

    def __set__(self, instance, value: float) -> None:
        if value < 0:
            raise ValueError("Значение не может быть отрицательным")
        setattr(instance, self.private_name, value)

    def __delete__(self, instance) -> None:
        delattr(instance, self.private_name)


class Person:
    age = PositiveNumber()
    balance = PositiveNumber()

    def __init__(self, name: str, age: int, balance: float = 0) -> None:
        self.name = name
        self.age = age
        self.balance = balance


person = Person("Viktor", 48, 1000)

print(person.age)        # 48
print(person.balance)    # 1000
print(person.__dict__)   # {'name': 'Viktor', '_age': 48, '_balance': 1000}

person.balance = 1500
print(person.balance)    # 1500

# person.age = -10       # ValueError: Значение не может быть отрицательным

Значения лежат в _age и _balance, а age и balance — дескрипторы, управляющие доступом.