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

Урок 70. Наследование и полиморфизм

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

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

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

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

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

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

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

less_36__Inheritance_and_polymorphism/theory_01__4_pillars_of_the_OOP.md

Даёт по одному самостоятельному примеру на каждый из четырёх принципов: BankAccount с приватным __balance для инкапсуляции, ABC-класс Shape с @abstractmethod area() для абстракции, Vehicle → Car для наследования и Cat/Dog с одинаковым по имени методом sound() для полиморфизма. В отличие от урока 66 (там все четыре принципа разбирались на ОДНОМ примере Shape), здесь под каждый принцип — свой отдельный, более наглядный пример.

  • Инкапсуляция: BankAccount с self.__balance и методами deposit/withdraw/get_balance
  • Абстракция: Shape(ABC) с @abstractmethod area(), реализации в Circle и Square
  • Наследование: Car(Vehicle) наследует drive(), добавляет свой honk()
  • Полиморфизм: Cat.sound()/Dog.sound() — один интерфейс, разное поведение
  • Итоговая сводка: что означает каждый принцип одной фразой
Показать начало файла (131 строк всего)
## 4 столпа (кита, основы) ООП

### 1. Инкапсуляция (Encapsulation)

**Смысл:** 
1. Скрытие внутренней реализации объекта и 
2. предоставление доступа к данным через методы.

```python
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # приватный атрибут

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount

    def get_balance(self):
        return self.__balance

account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # 1500
# print(account.__balance)    # Ошибка! Доступ запрещён
```

> Здесь баланс скрыт, и доступ к нему идёт только через методы класса.

---

### 2. Абстракция (Abstraction)

**Смысл:** 
* Отделение сути объекта от деталей реализации. 
* Мы работаем с объектом через его интерфейс, не заботясь о его внутренней "кухне".

```python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2
    

class Square(Shape):
    def __init__(self, side):
        self.side = side

…
Проверьте себя: Почему `print(account.__balance)` в примере BankAccount вызывает ошибку, хотя `account.get_balance()` работает и возвращает то же значение?
2
MarkdownРазбор концепции131 строк

super(): гибкий вызов метода родителя вместо жёсткого имени класса

less_36__Inheritance_and_polymorphism/theory_02__super.md

Сравнивает два способа вызвать метод родителя из наследника: «жёсткий» — Animal.__init__(self, name) с явным именем класса, и «гибкий» — super().__init__(name), не привязанный к конкретному имени родителя. Объясняет, почему super() предпочтительнее: он следует MRO (Method Resolution Order) и корректно работает при множественном наследовании. Пример с четырьмя классами A/B/C/D(B, C) показывает порядок вызова say() через цепочку super() — D → B → C → A, подтверждённый выводом D.mro().

  • «Жёсткий» способ: Animal.__init__(self, name) внутри Dog.__init__
  • «Гибкий» способ: super().__init__(name) и super().speak()
  • Dog.speak() сначала вызывает родительский speak(), потом добавляет своё
  • Пример множественного наследования: D(B, C), каждый say() зовёт super().say()
  • d.say() печатает D, B, C, A — порядок из D.mro()
Показать начало файла (131 строк всего)
### 1. Что такое `super()`

`super()` — это встроенная функция, которая позволяет 
* обратиться к родительскому классу из класса наследника
* и вызвать его (родителя) методы.

Функция `super()` полезна, когда:

* Нужно расширить поведение родительского метода, а не полностью его заменять.
* Есть **множественное наследование**, и порядок вызова методов (`MRO`) важен.

---

### 2. Пример "жёсткого" базового наследования (неправильно!)

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} издает звук")

class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(self, name)  # вызов конструктора родителя
        self.breed = breed

    def speak(self):
        Animal.speak(self)  # вызов метода speak родителя
        print(f"{self.name} говорит: Гав!")
```

---

### 3. Пример "гибкого" базового наследования (правильный)

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} издает звук")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # вызов конструктора родителя
        self.breed = breed

    def speak(self):
        super().speak()  # вызов метода speak родителя
        print(f"{self.name} говорит: Гав!")

dog = Dog("Шарик", "Дворняжка")
dog.speak()

# Шарик издает звук
# Шарик говорит: Гав!
```
…
Проверьте себя: Почему d.say() выводит именно порядок D, B, C, A, а не D, B, A, C — что определяет этот порядок?
3
MarkdownРазбор концепции78 строк

isinstance() и issubclass(): проверка объекта и проверка класса

less_36__Inheritance_and_polymorphism/theory_03__isinstance_and_issubclass.md

Разбирает две функции интроспекции: isinstance(obj, classinfo) проверяет принадлежность ОБЪЕКТА классу или любому из его предков по всей иерархии наследования, а issubclass(class, classinfo) проверяет отношение между двумя КЛАССАМИ напрямую. Обе функции принимают либо один класс, либо кортеж классов в качестве второго аргумента.

  • isinstance(dog, Dog) / isinstance(dog, Animal) — True по всей иерархии
  • isinstance(dog, (int, Animal, str)) — проверка сразу по нескольким классам
  • issubclass(Dog, Animal) → True, issubclass(Animal, Dog) → False
  • issubclass(Dog, (int, Animal)) — тоже принимает кортеж
  • Сводная таблица: isinstance проверяет объект, issubclass — класс
Показать начало файла (78 строк всего)
### 1. `isinstance(obj, classinfo)`

**Назначение:** 
* Проверяет, является ли объект `obj` 
  * экземпляром класса `classinfo` 
  * или его подклассов.

**Синтаксис:**

```python
isinstance(obj, classinfo)
```

* `obj` — объект, который проверяем.
* `classinfo` — класс или тюпл классов.

**Примеры:**

```python
class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

print(isinstance(dog, Dog))      # True — это экземпляр Dog
print(isinstance(dog, Animal))   # True — Dog наследует Animal
print(isinstance(dog, int))      # False — это не число

# Можно проверять несколько классов сразу
print(isinstance(dog, (int, Animal, str)))  # True, так как dog — Animal
```

**Особенность**: проверка работает **по всей иерархии наследования**.

---

### 2. `issubclass(class, classinfo)`

**Назначение:** Проверяет, является ли класс `class` **подклассом** `classinfo`.

**Синтаксис:**

```python
issubclass(class, classinfo)
```

* `class` — проверяемый класс.
* `classinfo` — класс или тюпл классов.

**Примеры:**

```python
class Animal:
    pass

class Dog(Animal):
    pass
…
Проверьте себя: Почему isinstance(dog, Animal) возвращает True, если dog — экземпляр именно Dog, а не Animal напрямую?
4
MarkdownРазбор концепции49 строк

hasattr(): безопасная проверка наличия атрибута или метода

less_36__Inheritance_and_polymorphism/theory_04__hasattr.md

Объясняет hasattr(object, name) — функцию, которая возвращает True/False в зависимости от наличия у объекта атрибута с заданным именем, не бросая AttributeError. Уточняет, что hasattr работает одинаково и для данных, и для методов, поскольку методы тоже являются атрибутами объекта, и особенно полезна при наследовании и динамической работе с объектами, чей набор атрибутов заранее не известен.

  • hasattr(p, 'name') — True, атрибут есть
  • hasattr(p, 'age') — False, атрибута нет
  • hasattr(p, 'greet') — True, метод — тоже атрибут
  • Применение: избежать AttributeError при наследовании и динамической работе с объектами
Показать начало файла (49 строк всего)
### Функция `hasattr`

Используется для проверки, **существует ли у объекта атрибут с указанным именем**.

---

#### Синтаксис

```python
hasattr(object, name)
```

* `object` — объект, у которого проверяется наличие атрибута.
* `name` — имя атрибута в виде строки.

**Возвращает:**

* `True`, если атрибут существует.
* `False`, если атрибута нет.

---

#### Пример

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

    def greet(self):
        print(f"Hello, my name is {self.name}")

p = Person("Alice")

# Проверяем существование атрибутов
print(hasattr(p, "name"))   # True
print(hasattr(p, "age"))    # False
print(hasattr(p, "greet"))  # True
```

---

#### Особенности

1. Работает не только с атрибутами, но и с методами, т.к. методы в Python тоже являются атрибутами объекта (в широком смысле).
2. Полезна при наследовании, чтобы избежать ошибки `AttributeError` (при вызове несуществующего атрибута)
3. Очень полезна при **динамической работе с объектами**, когда заранее неизвестны все атрибуты.

Проверьте себя: Почему hasattr(p, 'greet') возвращает True, хотя greet — это метод, а не переменная, присвоенная через self в __init__?
5
MarkdownРазбор концепции34 строк

Загадка: почему Student.population считает и себя, и родителя Person

less_36__Inheritance_and_polymorphism/theory_05__deep_understanding_inheritance.md

Постановка задачи-загадки перед объяснением в следующем файле: Student(Person) — пустой подкласс без своего __init__, но при создании p1, p2 (объекты Person) и s1 (объект Student) итоговый Student.population оказывается равен 3, а не 1, как можно было бы наивно ожидать. Файл специально оставляет вопрос открытым — читателю предлагается подумать, наследуются ли атрибуты класса «как надо» и как исправить population для Student, прежде чем читать разбор.

  • class Person: population = 0 (атрибут класса) + self.__class__.population += 1 в __init__
  • class Student(Person): pass — пустой наследник без переопределений
  • p1, p2 = Person(...), s1 = Student(...)
  • print(Student.population) — неожиданно 3, а не изолированный счётчик
  • Два открытых вопроса: наследуются ли атрибуты класса и как исправить Student.population
Показать начало файла (34 строк всего)
### Почему Student не считает свои объекты?

```python
class Person:
    population = 0

    def __init__(self, name):
        self.name = name
        self.__class__.population += 1

    @classmethod
    def get_population(cls):
        return cls.population


class Student(Person):
    pass


p1 = Person("John")
p2 = Person("Mary") 
s1 = Student("Peter")
print(Student.population)   # 3
```

Мы наследовали в классе Student все атрибуты и методы класса Person.

Тем не менее у Student `population` работает "неправильно":
* почему-то сосчитал и свои объекты, и объекта класса-родителя.

1. Неужели атрибуты класса не передаются по наследству (в отличие от методов)?
   * (Или же здесь какая-то иная причина?)

2. Как изменить код, чтобы атрибут класса Student `population` заработал-таки правильно?
Проверьте себя: Что вы ожидали бы увидеть в Student.population, если бы атрибуты класса копировались при наследовании, а не искались через MRO?
6
MarkdownРазбор концепции53 строк

Разбор загадки: атрибуты класса не копируются, self.__class__.population создаёт новый атрибут в дочернем классе

less_36__Inheritance_and_polymorphism/theory_06__deep_understanding_inheritance_explanation.md

Даёт полный ответ на вопрос предыдущего файла с трассировкой через id(): пока у Student нет собственного атрибута population, обращение Student.population находит атрибут в родительском Person через MRO — и id(Student.population) == id(Person.population) даёт True. Но как только создаётся первый экземпляр Student, строка self.__class__.population += 1 (где self.__class__ теперь Student) создаёт НОВЫЙ атрибут population именно в классе Student — с этого момента id(...) даёт False, и Student.population и Person.population начинают жить отдельно.

  • p1 = Person('John') — Student.population и Person.population совпадают по id, оба 1
  • p2 = Person('Mary') — совпадение id сохраняется, оба 2
  • s1 = Student('Peter') — id(Student.population) == id(Person.population) становится False
  • После s1: Student.population = 3, Person.population остаётся 2 — атрибуты разошлись
  • Вывод: self.__class__.population += 1 создаёт population в САМОМ дочернем классе, а не в Person
Показать начало файла (53 строк всего)
### Почему Student не считает свои объекты?

```python
class Person:
    population = 0

    def __init__(self, name):
        self.name = name
        self.__class__.population += 1

    @classmethod
    def get_population(cls):
        return cls.population


class Student(Person):
    pass


p1 = Person("John")
print(id(Student.population) == id(Person.population))
print("Student: ", Student.population, "Person", Person.population)

p2 = Person("Mary")
print(id(Student.population) == id(Person.population))
print("Student: ", Student.population, "Person", Person.population)

s1 = Student("Peter")
print(id(Student.population) == id(Person.population))
print("Student: ", Student.population, "Person", Person.population)


# True
# Student:  1 Person 1
# True
# Student:  2 Person 2
# False
# Student:  3 Person 2
```

## Пояснения

Атрибуты классов не копируются при наследовании.  
Они ищутся через MRO (цепочку наследования).  

Пока у дочернего класса `Student` нет собственного атрибута `population`,   
обращение к `Student.population` находит атрибут в родительском классе `Person`.

Но как только создаётся первый экземпляр дочернего класса, операция присваивания 
```python
self.__class__.population += 1
``` 
тут же создаёт новый атрибут класса `population` именно в дочернем классе `Student`.
Проверьте себя: На каком именно шаге (создание p1, p2 или s1) id(Student.population) и id(Person.population) перестают совпадать — и что конкретно в коде это вызывает?