📦 Репозиторий занятия 74

Урок 74. Инкапсуляция

Как работать с репозиторием

Каждая карточка говорит, о чём файл, что он выводит и что в нём искать. Код виден прямо здесь: его можно скопировать одной кнопкой, скачать файл или открыть его целиком.

Маршрут изучения

  1. Прочитайте описание: по нему уже понятно, о чём файл и что он выведет.
  2. Предскажите вывод: сравните своё предположение со строкой «Что выводит».
  3. Запустите: скачайте файл или скопируйте код кнопкой и выполните его у себя.
  4. Измените: поменяйте одно условие или значение и объясните новый результат.

Файлы: рекомендуемый порядок

1
MarkdownРазбор концепции96 строк

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

less_38___encapsulation_2/theory_01__public_protected_and_private_attrs.md

Раскрывает второй, более строгий аспект инкапсуляции — «спрятать, чтобы не брали без спросу», в отличие от первого («убрать с глаз долой», атрибуты как часть объекта). Разбирает три уровня доступа: публичные атрибуты (без подчёркивания, свободный доступ отовсюду), защищённые (`_name`, соглашение «не трогать снаружи», но Python не запрещает) и приватные (`__name`, Python применяет name mangling — переименовывает атрибут в `_ClassName__attribute`, что затрудняет, но не исключает случайный доступ извне).

  • Два аспекта инкапсуляции: «убрать с глаз долой» vs «спрятать, чтобы не брали без спросу»
  • Публичный атрибут: self.name — доступен отовсюду
  • Защищённый атрибут: self._name — соглашение, не строгий запрет
  • Приватный атрибут: self.__name — name mangling в _ClassName__attribute
  • p._Person__name — обход name mangling, доступ всё же возможен
  • Итоговая таблица трёх уровней доступа
Показать начало файла (96 строк всего)
## Публичные, защищённые и приватные аттрибуты 

Инкапсуляция означает:
* Скрыть (упаковать, спрятать) что-то внутрь какой-то оболочки (капсулы)

В ООП (вообще и в Python в частности) под инкапсуляцией понимается
* размещение данных и/или логики внутрь объекта, 
* чтобы при необходимости легко получить (вызвать) через dot-notation

Сам смысл этого "сокрытия лишнего" можно рассматривать (понимать) в двух аспектах:

1. "Убрать с глаз долой, чтобы не мешалось"
  * Атрибуты создаются в `__init__()` и затем не требуют специальной передачи в методы
  * поскольку уже и так становятся часть объекта

2. "Спрятать, чтобы не брали без спросу"
  * Получение и/или изменение данных возможно ТОЛЬКО при соблюдении определённых условий


Вот именно второй аспект понятия "Инкапсуляция" мы и рассмотрим в этом блоке.


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

**Принцип**: *"Приходи кто хочешь, и бери сколько захочешь"*

* Доступны **из любого места**: внутри класса, из подклассов, из внешнего кода.
* Обычно **не имеют специальных подчёркиваний**.
* Используются по умолчанию.


**Пример:**

```python
class Person:
    def __init__(self, name):
        self.name = name  # публичный атрибут

p = Person("Alice")
print(p.name)  # доступно извне
```

---

### 2. Защищённые атрибуты (`_protected attributes`)

**Принцип**: *"Приходи кто хочешь, но бери только для внутреннего использования"*

* Обозначаются **одним подчёркиванием `_` перед именем**: `_attribute`.
* **Соглашение**: к ним **не следует обращаться извне класса**, но Python не запрещает это.
* Часто используются для **наследования** и для внутреннего контроля.

**Пример:**

```python
class Person:
    def __init__(self, name):
        self._name = name  # защищённый атрибут

p = Person("Bob")
…
Проверьте себя: Как именно называется реальный ключ в __dict__ объекта после присвоения self.__name = name внутри класса Person, и почему просто p.__name к нему не обращается?
2
MarkdownРазбор концепции102 строк

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

less_38___encapsulation_2/theory_02__public_protected_and_private_methods.md

Переносит ту же трёхуровневую модель доступа (public/protected/private) с атрибутов на методы: публичный start_engine() вызывается откуда угодно, защищённый _check_fuel() по соглашению используется только внутри класса и подклассов (ElectricCar может его вызвать), а приватный __update_software() из-за name mangling недоступен по прямому имени даже подклассам и обычно вызывается лишь внутри самого класса — но всё равно достижим через car._Car__update_software().

  • Публичный метод: car.start_engine() — без ограничений
  • Защищённый метод: _check_fuel(), вызывается ElectricCar(Car) внутри battery_status()
  • Приватный метод: __update_software() — car.__update_software() бросает ошибку
  • Обход манглинга: car._Car__update_software() работает, но не рекомендуется
  • Вопрос «на засыпку»: какие ключи в __dict__ у объекта с self.__secret_code
  • Итоговая таблица доступа: из класса / из подкласса / из внешнего кода
Показать начало файла (102 строк всего)
## Публичные, защищённые и приватные методы 

Методы, как и атрибуты, также могут быть 
* **публичными** (public)
* **защищёнными** (protected),
* или **приватными** (private)

---

### 1. Публичные методы (public)

* Доступны **везде**: внутри класса, из подклассов и из внешнего кода.
* Нет специальных ограничений на вызов.
* Обычно применяются для основной функциональности класса.

**Пример:**

```python
class Car:
    def start_engine(self):
        print("Двигатель запущен!")

car = Car()
car.start_engine()  # можно вызвать из вне класса
```

> ✅ Доступно везде, без ограничений.

---

### 2. Защищённые методы (protected)

* Предназначены для использования **только внутри класса и его подклассов**.
* В Python нет строгой защиты; используется **одно подчёркивание** `_method()` по соглашению.
* Внешний код может вызвать метод, но это считается плохой практикой.

**Пример:**

```python
class Car:
    def _check_fuel(self):
        print("Проверка топлива")

class ElectricCar(Car):
    def battery_status(self):
        self._check_fuel()  # допустимо, внутри подкласса

car = Car()
car._check_fuel()  # можно вызвать, но не рекомендуется
```

> ⚠️ Это **соглашение**, а не строгая защита.

---

### 3. Приватные методы (private)

* Доступны **только внутри класса**, не видны подклассам.
* В Python используется **двойное подчёркивание** `__method()`.
* При этом Python делает **имя метода уникальным (name mangling)**, чтобы скрыть его.
…
Проверьте себя: Почему защищённый метод _check_fuel() доступен из подкласса ElectricCar, а приватный __update_software() — уже нет?
3
MarkdownРазбор концепции88 строк

Геттеры и сеттеры: от обычных методов get_/set_ к @property

less_38___encapsulation_2/theory_03__getter_setter.md

Показывает эволюцию контролируемого доступа к атрибуту: сначала обычные методы get_age()/set_age() с проверкой отрицательного значения внутри set_age, затем более Pythonic вариант через декораторы @property и @age.setter — тот же контроль, но обращение к p.age выглядит как к обычному полю. Подчёркивает, что геттер обязан ВОЗВРАЩАТЬ значение, а не просто печатать его, и что @property-версия при попытке присвоить некорректное значение бросает ValueError вместо простого предупреждения.

  • Обычные методы: get_age()/set_age(age) с if age < 0: print(...)
  • @property def age(self) — геттер, вызывается как p.age без скобок
  • @age.setter def age(self, value) — сеттер, срабатывает при p.age = 30
  • p.age = -5 с @property — бросает ValueError (строже, чем print-предупреждение)
  • Итоговая таблица: геттер/сеттер — что делает и какой синтаксис
Показать начало файла (88 строк всего)
### 1. Что такое геттер и сеттер?

* **Геттер (getter)** — метод, который **возвращает значение атрибута**.
  * геттер ОБЯЗАТЕЛЬНО возвращает значение атрибута 
    * (а не выводит на печать, или НЕ ТОЛЬКО выводит на печать)
* **Сеттер (setter)** — метод, который **изменяет значение атрибута**, 
  * при этом можно **контролировать или проверять данные** перед присвоением.

Идея: даже если атрибут скрыт (`protected` или `private`), мы можем управлять доступом к нему через методы класса.

---

### 2. Пример с обычными методами

```python
class Person:
    def __init__(self, name, age):
        self.__name = name      # приватный атрибут
        self.__age = age        # приватный атрибут

    # Getter
    def get_age(self):
        return self.__age

    # Setter
    def set_age(self, age):
        if age < 0:
            print("Возраст не может быть отрицательным!")
        else:
            self.__age = age

p = Person("Alice", 25)
print(p.get_age())  # 25

p.set_age(30)
print(p.get_age())  # 30

p.set_age(-5)       # Выведет предупреждение
```

✅ Такой подход **защищает данные от некорректного изменения**.

---

### 3. Pythonic способ через @property

В Python есть более элегантный способ — **декораторы `@property` и `@...setter`**.

```python
class Person:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    @property
    def age(self):          # getter
        return self.__age

    @age.setter
    def age(self, value):   # setter
…
Проверьте себя: Чем поведение set_age(-5) (обычный метод) отличается от p.age = -5 при использовании @property/@age.setter — что произойдёт в каждом случае?
4
MarkdownРазбор концепции164 строк

Дескрипторы: обобщение @property для переиспользуемой логики контроля атрибутов

less_38___encapsulation_2/theory_04__descriptor.md

Начинается с демонстрации реальной уязвимости @property-подхода: присвоение self.__age = age напрямую в __init__ обходит логику @age.setter, и Person(-10) создаётся без ошибки — исправление в том, чтобы писать self.age = age (через сеттер). Дальше вводится классический дескриптор — класс Age с методами __get__/__set__, который централизует контроль атрибута age для класса Person: теперь Person(-10) бросает ValueError уже на этапе создания. Финальный раздел решает проблему хардкода имени атрибута через __set_name__ и getattr/setattr/delattr в универсальном PositiveNumber, который можно переиспользовать для нескольких разных атрибутов (age, balance) без дублирования кода.

  • Ловушка: self.__age = age в __init__ обходит @age.setter, Person(-10) не падает
  • Исправление: self.age = age в __init__ — теперь сеттер срабатывает всегда
  • Классический дескриптор: class Age с __get__(self, instance, owner) и __set__(self, instance, value)
  • age = Age() в Person — имя атрибута должно совпадать с именем дескриптора
  • PositiveNumber с __set_name__/getattr/setattr/__delete__ — универсальный дескриптор без хардкода имени
  • Person с age = PositiveNumber() и balance = PositiveNumber() — переиспользование одной логики
Показать начало файла (164 строк всего)
### 1. Проблема с обычными геттерами и сеттерами

Представим класс с приватным атрибутом и сеттером, который проверяет возраст:

```python
class Person:
    def __init__(self, age):
        self.__age = age  # присвоили напрямую (обошли логику проверки из @age.setter)

    @property
    def age(self):
        return self.__age

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

---

### 2. Что происходит при создании объекта?

```python
p = Person(-10)  # прямо при __init__ присвоили "неверный" возраст
print(p.age)     # -10, сеттер не сработал!
```

✅ Внимание: проверка в сеттере **не сработала**, потому что мы обошли её, присвоив значение напрямую в `__init__`.


### 3. Более корректный способ использования геттера и сеттера

Прямое обращение к приватному атрибуту в `__init__` позволяет обойти логику проверки, описанную в сеттере. 

Поэтому, доступ к атрибутам экземпляра класса лучше НЕ ДЕЛАТЬ в обход сеттера:

```python
class Person:
    def __init__(self, age):
        self.age = age  # Теперь доступ СТРОГО через сеттер @age.setter

    @property
    def age(self):
        return self.__age

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


p = Person(-10)  # ValueError!
```

Как видим, теперь защита срабатывает уже на этапе создания объекта.

---
…
Проверьте себя: Почему в классическом дескрипторе Age внутри __set__ используется instance._age, а не instance.age — что случится, если использовать instance.age?