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

Урок 66. Основы ООП

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

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

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

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

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

1
PythonИсполняемый пример68 строк

Задача «до ООП»: функции и данные фигур по отдельности

less_34__OOP/theory_01_OOP_00.py

Отправная точка урока: набор независимых функций площади и периметра для прямоугольника, квадрата и прямоугольного треугольника, никак не связанных с данными о конкретной фигуре. Показывает две попытки «собрать» данные и функции вместе без классов — через namedtuple с полями-функциями и через обычный dict, где значения — тоже функции, — и в обоих случаях стороны фигуры всё равно приходится передавать в функцию вручную второй раз.

  • rectangle_area/square_area/right_triangle_area — функции площади
  • rectangle_perimeter/square_perimeter/right_triangle_perimeter — функции периметра
  • Shape = namedtuple(...) с полями area/perimeter как функциями
  • rectangle_3_4.area(3, 4) — приходится дублировать стороны при вызове
  • Вариант 2: dict с теми же полями-функциями, тот же недостаток

Что выводит: 12 (площадь), затем 14 (периметр) — дважды подряд: сначала через namedtuple rectangle_3_4, затем через словарь rectangle_2.

Начало файла
"""ВВОДНЫЕ:
Предположим, стоит задача обсчитывать периметр и площадь
для ОЧЕНЬ большого числа геометрических фигур разных размеров.
"""

""" ======================= AREA ============================== """
from collections import namedtuple


def rectangle_area(side1, side2):
    """Calculate the area of a rectangle"""
    return side1 * side2

Показать файл целиком (68 строк)
"""ВВОДНЫЕ:
Предположим, стоит задача обсчитывать периметр и площадь
для ОЧЕНЬ большого числа геометрических фигур разных размеров.
"""

""" ======================= AREA ============================== """
from collections import namedtuple


def rectangle_area(side1, side2):
    """Calculate the area of a rectangle"""
    return side1 * side2


def square_area(side1):
    """Calculate the area of a square"""
    return side1 ** 2


def right_triangle_area(side1, side2):
    """Calculate the area of a right triangle"""
    return (side1 * side2) / 2


""" ======================= PERIMETR ============================ """


def rectangle_perimeter(side1, side2):
    """Calculate the perimeter of a rectangle"""
    return 2 * (side1 + side2)


def square_perimeter(side1):
    """Calculate the perimeter of a rectangle"""
    return 4 * side1


def right_triangle_perimeter(side1, side2):
    """Calculate the perimeter of a right triangle"""
    hypotenuse = (side1 ** 2 + side2 ** 2) ** 0.5
    return side1 + side2 + hypotenuse


""" ======================= FIGURES ============================ 
Как систематизировать все эти данные, чтобы было удобно
хранить информацию обо всех фигурах?
"""

# ===============  variant 1 =============

Shape = namedtuple('Shape',
                   ["side1", "side2", "area", "perimeter"])

rectangle_3_4 = Shape(3, 4, rectangle_area, rectangle_perimeter)
print(rectangle_3_4.area(3, 4))
print(rectangle_3_4.perimeter(3, 4))


# ===============  variant 2 =============

rectangle_2 = {
    "side1": 3,
    "side2": 4,
    "area": rectangle_area,
    "perimeter": rectangle_perimeter,
}
print(rectangle_2["area"](3, 4))
print(rectangle_2["perimeter"](3, 4))
Проверьте себя: В чём главное неудобство обоих вариантов (namedtuple и dict) — почему стороны фигуры приходится передавать дважды: при создании и при вызове area/perimeter?
Открыть файл →
2
PythonИсполняемый пример30 строк

Первый класс: Square хранит свои данные и умеет их использовать

less_34__OOP/theory_01_OOP_01.py

Решает проблему предыдущего файла: класс Square хранит side как атрибут экземпляра в __init__, а методы area() и perimeter() читают self.side без повторной передачи параметров. Комментарий подчёркивает главную выгоду объекта — «подставлять» значения в свои функции больше не нужно вручную.

  • class Square: __init__(self, side) — сохранение стороны в self.side
  • def area(self): return self.side ** 2
  • def perimeter(self): return self.side * 4
  • square = Square(3); square.area() — без повторной передачи стороны

Что выводит: 3 (square.side), затем 9 (square.area()), затем 12 (square.perimeter()).

Начало файла
"""
Поэтому, крайне удобно иметь объект, который:
- не только содержит все присущие ему значения и функции
- но и может "подставить" свои значения в свои функции без лишних "телодвижений".

И этот объект - экземпляр класса (class instance).
"""


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

    def area(self):
Показать файл целиком (30 строк)
"""
Поэтому, крайне удобно иметь объект, который:
- не только содержит все присущие ему значения и функции
- но и может "подставить" свои значения в свои функции без лишних "телодвижений".

И этот объект - экземпляр класса (class instance).
"""


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

    def area(self):
        return self.side ** 2

    def perimeter(self):
        return self.side * 4


square = Square(3)
print(square.side)        # 3
print(square.area())      # 9
print(square.perimeter()) # 12

"""
Объект square - квадрат со стороной 3 условных единицы.
Его площадь и его периметр рассчитываются автоматически, поскольку
"подставлять" значения в них НЕ НАДО!
"""
Проверьте себя: Почему square.area() не требует аргументов, хотя формула площади квадрата зависит от стороны?
Открыть файл →
3
PythonИсполняемый пример72 строк

Иерархия Shape → Rectangle/Square/RightTriangle и полиморфизм

less_34__OOP/theory_01_OOP_02.py

Вводит базовый класс Shape с методами-заглушками area()/perimeter() (pass) и три подкласса, каждый с собственным __init__ и переопределёнными area/perimeter — без использования super(). Демонстрирует служебные атрибуты объекта (__dict__, __dir__(), __doc__ из докстринга класса) и полиморфизм: функция print_shape_info(shape) одинаково работает с любым объектом-наследником Shape, вызывая его area()/perimeter() через единый интерфейс.

  • class Shape: area()/perimeter() — заглушки pass
  • Rectangle/Square/RightTriangle(Shape) — свой __init__ и свои area/perimeter у каждого
  • r1.__dict__, r1.__dir__(), r1.__doc__ — интроспекция объекта
  • print_shape_info(shape: Shape) — полиморфная функция для списка разных фигур
  • for shape in shapes: print_shape_info(shape)

Что выводит: {'side1': 3, 'side2': 4}; затем длинный список __dir__() (реально содержит все magic-методы объекта — заметно длиннее, чем упрощённый список в комментарии кода); докстринг Rectangle; затем блоки Area/Perimeter для Rectangle (12/14), Square (25/20) и RightTriangle (6.0/12).

Начало файла

class Shape:
    def area(self):
        pass

    def perimeter(self):
        pass


class Rectangle(Shape):
    """Initialize a Rectangle object with the given side lengths."""
    def __init__(self, side1, side2):
        self.side1 = side1
        self.side2 = side2
Показать файл целиком (72 строк)

class Shape:
    def area(self):
        pass

    def perimeter(self):
        pass


class Rectangle(Shape):
    """Initialize a Rectangle object with the given side lengths."""
    def __init__(self, side1, side2):
        self.side1 = side1
        self.side2 = side2

    def area(self):
        return self.side1 * self.side2

    def perimeter(self):
        return 2 * (self.side1 + self.side2)


class Square(Shape):
    """Initialize a Square object with the given side length."""
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

    def perimeter(self):
        return 4 * self.side


class RightTriangle(Shape):
    """Initialize a RightTriangle object with the given leg lengths."""
    def __init__(self, side1, side2, hypotenuse=None):
        self.side1 = side1
        self.side2 = side2
        self.hypotenuse = hypotenuse if hypotenuse is not None else (side1 ** 2 + side2 ** 2) ** 0.5

    def area(self):
        return (self.side1 * self.side2) / 2

    def perimeter(self):
        return self.side1 + self.side2 + self.hypotenuse


r1 = Rectangle(3, 4)
print(r1.__dict__)   # {'side1': 3, 'side2': 4}
print(r1.__dir__())  # ['side1', 'side2', '__doc__', 'area', 'perimeter']
print(r1.__doc__)


""" ====================== ПОЛИМОРФИЗМ (УНИФИКАЦИЯ) ======================= """


def print_shape_info(shape: Shape):
    print(f"===== {shape.__class__.__name__} =====")
    print(f"Area: {shape.area()}")
    print(f"Perimeter: {shape.perimeter()}")


shapes = [
    Rectangle(3, 4),
    Square(5),
    RightTriangle(3, 4, 5),
]

for shape in shapes:
    print_shape_info(shape)

Проверьте себя: Почему r1.__dir__() выводит гораздо больше имён, чем перечислено в комментарии кода (['side1', 'side2', '__doc__', 'area', 'perimeter']), и что это за дополнительные имена?
Открыть файл →
4
PythonИсполняемый пример47 строк

Та же иерархия фигур, но с super() вместо дублирования кода

less_34__OOP/theory_01_OOP_02_var_2.py

Переписывает предыдущий пример по-другому: базовый Shape теперь содержит реальную реализацию __init__/area/perimeter для двух сторон, а Rectangle просто наследует её без изменений. Square вызывает super().__init__(side, side), чтобы переиспользовать логику Shape с равными сторонами, а RightTriangle переопределяет area() через super().area() / 2 и добавляет собственную формулу перимет­ра с гипотенузой — иллюстрируя, как наследование избавляет от копирования кода между похожими классами.

  • class Shape: полноценные __init__/area/perimeter для двух сторон
  • class Rectangle(Shape): только докстринг, вся логика унаследована
  • class Square(Shape): __init__ вызывает super().__init__(side, side)
  • class RightTriangle(Shape): area() через super().area() / 2, свой perimeter()

Что выводит: {'side1': 3, 'side2': 4}, список __dir__(), докстринг Rectangle; затем {'side1': 3, 'side2': 3} для square, 9 (area), 12 (perimeter); затем 6.0 — area() прямоугольного треугольника через super().area() / 2.

Начало файла

class Shape:
    def __init__(self, side1, side2):
        self.side1 = side1
        self.side2 = side2

    def area(self):
        return self.side1 * self.side2

    def perimeter(self):
        return 2 * (self.side1 + self.side2)


class Rectangle(Shape):
Показать файл целиком (47 строк)

class Shape:
    def __init__(self, side1, side2):
        self.side1 = side1
        self.side2 = side2

    def area(self):
        return self.side1 * self.side2

    def perimeter(self):
        return 2 * (self.side1 + self.side2)


class Rectangle(Shape):
    """Initialize a Rectangle object with the given side lengths."""


class Square(Shape):
    """Initialize a Square object with the given side length."""

    def __init__(self, side):
        super().__init__(side, side)


class RightTriangle(Shape):
    """Initialize a RightTriangle object with the given leg lengths."""

    def area(self):
        return super().area() / 2

    def perimeter(self):
        hypotenuse = (self.side1 ** 2 + self.side2 ** 2) ** 0.5
        return self.side1 + self.side2 + hypotenuse


rectangle = Rectangle(3, 4)
print(rectangle.__dict__)   # {'side1': 3, 'side2': 4}
print(rectangle.__dir__())  # ['side1', 'side2', '__doc__', 'area', 'perimeter']
print(rectangle.__doc__)    # Initialize a Rectangle object with the given side lengths.

square = Square(3)
print(square.__dict__)   # {'side1': 3, 'side2': 3}
print(square.area())
print(square.perimeter())

rt = RightTriangle(3,4)
print(rt.area())
Проверьте себя: Почему в RightTriangle.area() достаточно вызвать super().area() / 2, а не переписывать формулу side1 * side2 / 2 заново?
Открыть файл →
5
MarkdownРазбор концепции90 строк

Четыре принципа ООП на примере иерархии Shape

less_34__OOP/theory_01_OOP_4_principles.md

Разбирает абстракцию, инкапсуляцию, полиморфизм и наследование на общем примере фигур из соседних файлов: Shape как абстрактный интерфейс с pass-методами, Rectangle с приватными по смыслу side1/side2, print_shape_info как единый интерфейс для разных подклассов, и Rectangle(Shape) как пример «заимствования, а не копирования» поведения. Отдельно подчёркнуто отличие «заимствовать» от «скопировать» — наследник получает доступ к атрибутам и методам родителя по ссылке, а не их дубликат.

  • Абстракция: Shape с методами-заглушками area()/perimeter()
  • Инкапсуляция: side1/side2 доступны через методы, а не напрямую
  • Полиморфизм: print_shape_info(shape: Shape) — единый интерфейс для всех форм
  • Наследование: Rectangle(Shape) — «заимствовать» ≠ «скопировать»
  • Итоговое сопоставление всех четырёх принципов с конкретным кодом примера
Показать начало файла (90 строк всего)
### 1. Абстракция

**Абстракция** заключается в выделении общих характеристик объектов  
и создании на их основе обобщённых представлений. 

В нашем примере класс `Shape` представляет абстрактное понятие формы,  
предоставляя общие методы `area` и `perimeter`, которые должны быть реализованы в подклассах.

```
class Shape:
    def area(self):
        pass

    def perimeter(self):
        pass
```

### 2. Инкапсуляция

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

В нашем примере атрибуты `side1` и `side2` класса `Rectangle` являются частными,  
и доступ к ним осуществляется через методы `area` и `perimeter`.

```
class Rectangle(Shape):
    def __init__(self, side1, side2):
        self.side1 = side1
        self.side2 = side2

    def area(self):
        return self.side1 * self.side2

    def perimeter(self):
        return 2 * (self.side1 + self.side2)
```

### 3. Полиморфизм (унификация)

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

В нашем примере методы `area` и `perimeter` могут быть вызваны как у объекта класса `Rectangle`,  
так и у любого другого подкласса `Shape`, реализующего эти методы.  

Таким образом, мы можем использовать объекты разных форм через единый интерфейс.

```
def print_shape_info(shape: Shape):
    print(f"Area: {shape.area()}")
    print(f"Perimeter: {shape.perimeter()}")

rectangle = Rectangle(3, 4)
print_shape_info(rectangle)
```

### 4. Наследование

**Наследование** позволяет создавать новые классы на основе существующих,  
…
Проверьте себя: В чём разница между «заимствовать» и «скопировать» применительно к наследованию Rectangle от Shape — что именно означает доступ «по ссылке»?
6
PythonИсполняемый пример28 строк

Атрибут класса vs атрибут экземпляра: ловушка присваивания

less_34__OOP/theory_03_class_attr_vs_instance_attr.py

Класс Car хранит общий счётчик quantity как атрибут класса, увеличиваемый в __init__ у каждого нового экземпляра — и это значение видно одинаково через любой объект. Ключевой момент: присваивание toyota.quantity = 10 НЕ меняет атрибут класса, а создаёт новый атрибут экземпляра, который «экранирует» доступ к quantity класса именно для toyota — что подтверждается разницей между __dict__ двух экземпляров и обходом через toyota.__class__.quantity.

  • class Car: quantity = 0 (атрибут класса) + Car.quantity += 1 в __init__
  • wv = Car('WV'), toyota = Car('Toyota') — общий счётчик растёт на оба экземпляра
  • toyota.quantity = 10 — создаёт НОВЫЙ атрибут экземпляра, не трогая класс
  • wv.__dict__ vs toyota.__dict__ — видно появление лишнего ключа quantity у toyota
  • toyota.__class__.quantity — обход экранирования, доступ к настоящему атрибуту класса

Что выводит: 1, затем 2, затем 2 (wv.quantity тоже стал 2 — общий атрибут класса); затем 10 (toyota.quantity после присваивания); {'brand': 'WV'}; {'brand': 'Toyota', 'quantity': 10}; и в конце 2 — реальное значение Car.quantity, доступное через __class__.

Начало файла
class Car:
    quantity = 0

    def __init__(self, brand):
        self.brand = brand
        Car.quantity += 1


wv = Car('WV')
print(wv.quantity)  # 1
toyota = Car('Toyota')
print(toyota.quantity)  # 2
print(wv.quantity)      # 2
Показать файл целиком (28 строк)
class Car:
    quantity = 0

    def __init__(self, brand):
        self.brand = brand
        Car.quantity += 1


wv = Car('WV')
print(wv.quantity)  # 1
toyota = Car('Toyota')
print(toyota.quantity)  # 2
print(wv.quantity)      # 2

# Попробуем изменить атрибут класса из экземпляра класса
toyota.quantity = 10
print(toyota.quantity)  # 10

# Попробуем значения атрибутов экземпляров класса
print(wv.__dict__)       # {'brand': 'WV'}
print(toyota.__dict__)   # {'brand': 'Toyota', 'quantity': 10}

# Вывод: мы НЕ изменили атрибут класса quantity
# Мы просто добавили новый атрибут к экземпляру класса toyota
# Который теперь закрывает (экранирует) доступ к атрибуту класса quantity
# из экземпляра класса.
# Но мы можем обойти экранирование, обратившись к классу с помощью метода .__class__
print(toyota.__class__.quantity)  # 2
Проверьте себя: Почему после toyota.quantity = 10 значение wv.quantity остаётся равным 2, а не тоже становится 10?
Открыть файл →