Объектно-ориентированное программирование в Python

Объектно-ориентированное программирование в Python

Объектно-ориентированное программирование (ООП) — это способ написания кода, который позволяет представлять данные и действия над ними в виде объектов. Каждый объект представляет собой некоторую сущность, которую можно описать с помощью свойств и методов.

Содержание
  1. Основные понятия объектно-ориентированного программирования
  2. Класс
  3. Объекты
  4. Атрибуты
  5. Методы
  6. Конструктор и деструктор
  7. Ключевое слово self
  8. Основные принципы ООП
  9. Наследование
  10. Ключевое слово super
  11. Множественное наследование
  12. Инкапсуляция
  13. Полиморфизм
  14. Абстракция
  15. Создание собственных операторов
  16. Имена методов для перегрузки стандартных операторов
  17. Перегрузка операторов индексации
  18. Пример реализации ООП
  19. Специальные методы в классах
  20. Примеры работы с «магическими методами».
  21. Контекстный менеджер в Python
  22. Примеры работы контекстных менеджеров
  23. Защищенные и приватные методы для реализации инкапсуляции
  24. Геттер, сеттер и делитер. Способы работы с защищенными и приватными методами и атрибутами.
  25. Статические методы и атрибуты
  26. Реализация абстракции через ABC
  27. Перечисляемые типы
  28. Классы для хранения данных. Декоратор @dataclass
  29. Задания для подготовки

Основные понятия объектно-ориентированного программирования

Класс

Класс — это определение объекта, которое содержит набор атрибутов (переменных) и методов (функций). Он представляет собой тип объекта, а объекты этого класса являются экземплярами этого типа. Класс можно рассматривать как чертеж, по которому создаются объекты.

Пример аналогии из материального мира: Класс можно сравнить с чертежом здания. Он содержит все необходимые инструкции для создания объектов.

Пример реализации на Python:

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

    def say_hello(self):
        print(f"Привет, меня зовут {self.name} и мне {self.age} лет.")

Объекты

Объект — это экземпляр класса. Он содержит значения атрибутов, определенных в классе, и может выполнять методы, определенные в классе.

Пример из реального мира. По чертежу из предыдущего примера построили несколько зданий. Причем здания имели уникальные свойства, которые отличали их друг от друга.

Пример2. Класс — автомобиль. Объекты: Lada X-Ray, Mercedes-Benz CLK

Пример реализации на Python:

person = Person("Иван", 30)
person.say_hello() # Вывод: Привет, меня зовут Иван и мне 30 лет.

Атрибуты

Атрибут — это переменная, определенная в классе, которая хранит значение, относящееся к объекту. Атрибуты могут быть публичными или приватными.

Пример аналогии из материального мира: Атрибут можно сравнить с характеристикой объекта, например, цветом или размером. Например, красный автомобиль или количество углов многоугольника

Пример реализации на Python:

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

car = Car("Toyota", "Camry", 2022)

Методы

Метод — это функция, которая определена внутри класса и может быть вызвана у объектов этого класса. Методы в классах описывают действия, которые объекты могут выполнять.

Давай рассмотрим пример с классом «Животное». Представим, что у нас есть класс «Животное», атрибутами которого являются имя животного и его возраст. Методом может быть, например, метод «голос», который описывает звук, который издает животное. Этот метод может иметь доступ к атрибутам класса, таким как имя животного.

Пример кода на Python:

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

    def sound(self):
        pass  # этот метод будет определен в дочерних классах


class Cat(Animal):
    def sound(self):
        return "Meow"


class Dog(Animal):
    def sound(self):
        return "Woof"


cat = Cat("Tom", 3)
dog = Dog("Rex", 5)

print(cat.sound())  # выводит "Meow"
print(dog.sound())  # выводит "Woof"

Здесь мы определяем класс «Животное» с методом «звук», который не имеет реализации и будет переопределен в дочерних классах. Затем мы определяем дочерние классы «Кошка» и «Собака», которые наследуют класс «Животное». В каждом дочернем классе мы переопределяем метод «звук», чтобы он возвращал соответствующий звук для каждого класса. Наконец, мы создаем объекты классов «Кошка» и «Собака» и вызываем их методы «звук». В результате мы получаем разные звуки для каждого объекта в зависимости от его класса.

Конструктор и деструктор

Конструктор и деструктор являются специальными методами класса в Python, которые вызываются автоматически при создании и удалении экземпляров класса соответственно.

Конструктор класса обычно используется для инициализации экземпляра класса, установки его начальных значений. В Python, конструктором является метод с именем __init__, который автоматически вызывается при создании экземпляра класса.

Пример конструктора:

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

В данном примере, конструктор класса Person инициализирует атрибуты name и age для экземпляра класса при его создании.

Деструктор класса используется для освобождения ресурсов, которые были выделены для экземпляра класса, когда он больше не нужен и удаляется из памяти. В Python, деструктором является метод с именем __del__, который автоматически вызывается при удалении экземпляра класса.

Пример деструктора:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __del__(self):
        print(f"{self.name} удален из памяти")

person = Person("Alice", 25)
del person # "Alice удален из памяти" будет напечатано в консоль

Ключевое слово self

В примерах программ, в реализации методов, вы могли наблюдать  конструкции вида self

Ключевое слово self используется для обращения к атрибутам и методам класса из его методов. Когда мы создаем экземпляр класса и вызываем его методы, Python автоматически передает этот экземпляр в метод в качестве первого аргумента (названный self по соглашению). Это позволяет методам получать доступ к атрибутам и методам экземпляра класса.

Пример использования self:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def introduce(self):
        print(f"Привет, меня зовут {self.name} и мне {self.age} лет.")
        
person = Person("Alice", 25)
person.introduce() # "Привет, меня зовут Alice и мне 25 лет." будет напечатано в консоль

В данном примере, метод introduce класса Person использует атрибуты name и age, которые принадлежат конкретному экземпляру класса, доступ к которым он получает через self.

Основные принципы ООП

Наследование

Наследование — это один из основных принципов объектно-ориентированного программирования, который позволяет создавать новый класс на основе уже существующего класса, наследуя его свойства и методы.
Для того чтобы создать новый класс, который наследует свойства и методы от уже существующего класса, необходимо указать название базового класса в определении нового класса в качестве аргумента.

Например, если класс A — это базовый класс, а класс B наследует его свойства и методы, то определение класса B будет выглядеть следующим образом:

class A:
    def __init__(self, x):
        self.x = x
    
    def method_a(self):
        print("Метод A")
        
class B(A):
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y
        
    def method_b(self):
        print("Метод B")

В этом примере класс B наследует свойства и методы от класса A, который определяет атрибут x и метод method_a. При этом, класс B определяет свой собственный атрибут y и метод method_b.

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

Ключевое слово super

Ключевое слово super используется для вызова методов из базового класса в наследующем классе. Оно позволяет обращаться к методам базового класса, даже если их имена переопределены в классе-наследнике.

Множественное наследование

Наследование в Python поддерживает множественность, то есть класс может наследовать свойства и методы нескольких базовых классов. В этом случае определение класса-наследника будет выглядеть следующим образом:

class A:
    def method_a(self):
        print("Метод A")
        
class B:
    def method_b(self):
        print("Метод B")
        
class C(A, B):
    pass

Класс C наследует свойства и методы от классов A и B.

Инкапсуляция

Инкапсуляция – это принцип, который заключается в скрытии реализации от пользователя и предоставлении интерфейса для работы с объектом.

Другими словами, вместо того, чтобы напрямую обращаться к свойствам объекта, мы взаимодействуем с ним через его методы. Это позволяет упростить использование объекта и защитить его состояние от некорректного изменения.

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

Пример реализации на Python:

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.__odometer = 0

    def get_odometer(self):
        return self.__odometer

    def update_odometer(self, mileage):
        if mileage >= self.__odometer:
            self.__odometer = mileage
        else:
            print("You can't roll back an odometer!")

my_car = Car('Toyota', 'Corolla', 2020)
my_car.update_odometer(5000)
print(my_car.get_odometer())  # 5000

В примере мы создали класс Car, который имеет атрибуты make, model и year, а также методы get_odometer и update_odometer. Однако, атрибут __odometer был объявлен с двумя подчеркиваниями в начале, что делает его приватным, то есть скрытым от прямого доступа. Вместо этого мы создали методы get_odometer и update_odometer, которые позволяют получать и изменять значение этого атрибута через интерфейс объекта.

Полиморфизм

Полиморфизм — это способность объекта вести себя по-разному в зависимости от контекста использования. Это означает, что объект может быть использован в разных контекстах и при этом вести себя по-разному. В Python полиморфизм реализуется через использование одного и того же метода с различными параметрами.

Примером полиморфизма может служить функция len(), которая возвращает длину объекта в зависимости от типа объекта. Если объект является строкой, то len() возвращает количество символов в строке, если объект — списком, то длина списка и так далее.

Абстракция

Абстракция — это способ представления объектов и их поведения на более высоком уровне абстракции, чем конкретная реализация. Абстракция позволяет скрыть детали реализации и представить объекты в более удобной форме для пользователя. В Python абстракция реализуется через использование классов и интерфейсов.

Примером абстракции может служить класс Shape, который представляет абстрактную геометрическую фигуру. Этот класс может иметь различные методы, например, метод для вычисления площади фигуры или метод для вычисления периметра фигуры. Реализация методов может отличаться для каждого конкретного подкласса Shape, например, для классов Circle, Square и Triangle, но для пользователя эти подклассы выглядят как абстрактные геометрические фигуры со своими особенностями.

Создание собственных операторов

В Python есть возможность создания собственных операторов при реализации классов с помощью перегрузки методов. Перегрузка методов — это способ определения нового поведения для стандартных операторов языка (например, +, -, *, /, <, > и т.д.) для экземпляров классов.

Операторы могут быть перегружены с помощью специальных методов, которые начинаются и заканчиваются двойными подчеркиваниями (__). Например, чтобы определить поведение оператора + для экземпляров класса, можно определить метод add(). Этот метод принимает два аргумента — self (экземпляр класса) и other (другой объект, с которым производится операция сложения), и возвращает результат операции.

Например, рассмотрим класс Vector, который представляет собой вектор в трехмерном пространстве. Мы можем определить операторы сложения и вычитания для векторов следующим образом:

class Vector:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y, self.z - other.z)

Теперь мы можем складывать и вычитать экземпляры класса Vector, используя стандартные операторы + и -:

v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)

v3 = v1 + v2
v4 = v2 - v1

print(v3.x, v3.y, v3.z) # 5 7 9
print(v4.x, v4.y, v4.z) # 3 3 3

Также можно перегрузить другие операторы, например, операторы сравнения (<, >, <=, >=, ==, !=), операторы индексации ([]) и т.д.

Важно понимать, что перегрузка стандартных операторов должна использоваться только в тех случаях, когда это имеет смысл с точки зрения логики класса. Неправильное использование перегрузки может привести к неочевидным ошибкам в коде.

Имена методов для перегрузки стандартных операторов

Также можно перегрузить другие операторы, например, операторы сравнения (<, >, <=, >=, ==, !=), операторы индексации ([]) и т.д.

Важно понимать, что перегрузка стандартных операторов должна использоваться только в тех случаях, когда это имеет смысл с точки зрения логики класса. Неправильное использование перегрузки может привести к неочевидным ошибкам в коде.

  • __add__(self, other) для оператора сложения (+)
  • __sub__(self, other) для оператора вычитания (-)
  • __mul__(self, other) для оператора умножения (*)
  • __truediv__(self, other) для оператора деления (/)
  • __mod__(self, other) для оператора остатка от деления (%)
  • __pow__(self, other) для оператора возведения в степень (**)
  • __lt__(self, other) для оператора меньше (<)
  • __le__(self, other) для оператора меньше или равно (<=)
  • __eq__(self, other) для оператора равенства (==)
  • __ne__(self, other) для оператора неравенства (!=)
  • __gt__(self, other) для оператора больше (>)
  • __ge__(self, other) для оператора больше или равно (>=)

Эти методы должны быть определены в классе, который перегружает соответствующий оператор. Они принимают два аргумента — self и other, где self представляет объект, на котором вызывается оператор, а other представляет объект, с которым выполняется операция. Возвращаемое значение этих методов должно быть результатом выполнения соответствующей операции.

Перегрузка операторов индексации

Перегрузка оператора индексации [] позволяет создавать собственные объекты, которые можно обращать, как к элементам массива, используя квадратные скобки.

Пример реализации класса, который использует перегрузку оператора индексации:

class MyList:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        return self.items[index]

    def __setitem__(self, index, value):
        self.items[index] = value

my_list = MyList([1, 2, 3])
print(my_list[0])  # Output: 1
my_list[0] = 4
print(my_list[0])  # Output: 4

В данном примере класс MyList имеет атрибут items, который хранит элементы списка. Перегрузка оператора __getitem__ позволяет обращаться к элементам списка через квадратные скобки, как если бы объект MyList был списком. Перегрузка оператора __setitem__ позволяет изменять элементы списка через оператор присваивания =.

В данном примере my_list[0] вернет значение 1, а затем его значение будет изменено на 4 через оператор my_list[0] = 4. После этого my_list[0] вернет значение 4.

Пример реализации ООП

Рассмотрим следующие классы:

import math

# Абстрактный класс Фигура, содержит абстрактные методы для наследников
class Figure:
    def area(self):
        pass

    def perimeter(self):
        pass

# Класс Круг, является наследником класса Фигура
class Circle(Figure):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

# Класс Прямоугольник, является наследником класса Фигура
class Rectangle(Figure):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * (self.length + self.width)

# Класс Квадрат, является наследником класса Прямоугольник, используется полиморфизм
class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

# Класс Треугольник, является наследником класса Фигура
class Triangle(Figure):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def area(self):
        s = (self.a + self.b + self.c) / 2
        return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

    def perimeter(self):
        return self.a + self.b + self.c

# Пример использования классов
circle = Circle(5)
print("Площадь круга: ", circle.area())
print("Периметр круга: ", circle.perimeter())

rectangle = Rectangle(3, 4)
print("Площадь прямоугольника: ", rectangle.area())
print("Периметр прямоугольника: ", rectangle.perimeter())

square = Square(5)
print("Площадь квадрата: ", square.area())
print("Периметр квадрата: ", square.perimeter())

triangle = Triangle(3, 4, 5)
print("Площадь треугольника: ", triangle.area())
print("Периметр треугольника: ", triangle.perimeter())

В данном примере использованы все основные принципы ООП:

  • Наследование: классы Круг, Прямоугольник, Квадрат, Треугольник являются наследниками абстрактного класса Фигура.
  • Инкапсуляция: данные, необходимые для расчетов (радиус, стороны), хранятся внутри объектов классов, и доступ к ним осуществляется через методы.
  • Полиморфизм: методы area и perimeter рассчитывают площадь и периметр, исходя из особенностей фигуры
  • Абстракция: класс Figure — абстрактный, его методы не работают напрямую и созданы для реализации классов-потомков.

Также мы можем использовать полиморфизм для вывода информации о фигурах:

def print_shape_info(shape):
    print("Area: ", shape.area())
    print("Perimeter: ", shape.perimeter())

circle = Circle( 5)
rectangle = Rectangle(4, 6)

print_shape_info(circle)
print_shape_info(rectangle)

В этом примере мы определяем функцию print_shape_info(), которая принимает объект класса Figure. Эта функция выводит информацию о цвете, площади и периметре фигуры. Затем мы создаем объекты классов Circle и Rectangle и вызываем функцию print_shape_info() для каждого из них. Заметьте, что мы можем передавать объекты разных классов в эту функцию, потому что они оба наследуют класс Shape и реализуют методы area() и perimeter().

Также мы можем использовать перегрузку операторов для сравнения фигур:

class Circle(Shape):
    #...

    def __lt__(self, other):
        return self.radius < other.radius

class Rectangle(Shape):
    #...

    def __lt__(self, other):
        return self.area() < other.area()

circle1 = Circle(5)
circle2 = Circle( 3)
rectangle1 = Rectangle(4, 6)
rectangle2 = Rectangle(3, 5)

print(circle1 < circle2) # False
print(rectangle1 < rectangle2) # False
print(circle1 < rectangle1) # TypeError: '<' not supported between instances of 'Circle' and 'Rectangle'

Здесь мы определяем метод __lt__(), который позволяет сравнивать объекты

Специальные методы в классах

Специальные методы начинаются и заканчиваются двойным подчеркиванием, например, __init__ или __str__.

Эти методы предоставляют специальные функциональности для классов, что делает их более мощными и гибкими.

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

Специальные методы, также известные как магические методы или «dunder» (double underscore) методы, обычно вызываются автоматически в определенных ситуациях интерпретатором Python, и в большинстве случаев нет необходимости вызывать их явным образом.

Например, метод __init__ вызывается автоматически при создании нового экземпляра класса, а метод __str__ вызывается при использовании функции str() или при передаче объекта в функцию print().

Однако, если есть конкретная необходимость вызвать какой-то из этих методов явно, вы можете сделать это следующим образом:

class MyClass:
    def __init__(self, value):
        self.value = value

obj = MyClass(42)

# Вызов метода __init__ напрямую (не рекомендуется)
MyClass.__init__(obj, 10)

# Вызов метода __str__ напрямую
result = MyClass.__str__(obj)
print(result)

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

Примеры работы с «магическими методами».

  1. __init__(self, ...): Этот метод вызывается при создании нового объекта. Здесь инициализируются атрибуты объекта.
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    # Использование
    person = Person("John", 25)
    
  1. __str__(self): Возвращает строковое представление объекта. Часто используется для удобного вывода информации об объекте.
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __str__(self):
            return f"{self.name}, {self.age} years old"
    
    # Использование
    person = Person("John", 25)
    print(person)  # Вывод: John, 25 years old
    
  1. __len__(self): Возвращает длину объекта. Этот метод часто используется для контейнеров, таких как списки или строки.
    class ShoppingCart:
        def __init__(self, items):
            self.items = items
    
        def __len__(self):
            return len(self.items)
    
    # Использование
    cart = ShoppingCart(["item1", "item2", "item3"])
    print(len(cart))  # Вывод: 3
    
  2. __add__(self, other): Определяет поведение оператора сложения для объектов.
    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            return Point(self.x + other.x, self.y + other.y)
    
    # Использование
    point1 = Point(1, 2)
    point2 = Point(3, 4)
    result = point1 + point2
    print(result.x, result.y)  # Вывод: 4 6
    
  3. __repr__(self): Этот метод предоставляет «формальное» строковое представление объекта и используется функцией repr(). Он часто используется для отладочных целей.
    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    # Использование
    point = Point(1, 2)
    print(repr(point))  # Вывод: Point(1, 2)
    
  4. __getitem__(self, key) и __setitem__(self, key, value): Позволяют классу поддерживать доступ к элементам как к элементам последовательности (например, списку).
    class MyList:
        def __init__(self):
            self.items = []
    
        def __getitem__(self, index):
            return self.items[index]
    
        def __setitem__(self, index, value):
            self.items[index] = value
    
    # Использование
    my_list = MyList()
    my_list.items = [1, 2, 3]
    print(my_list[1])  # Вывод: 2
    my_list[1] = 42
    print(my_list.items)  # Вывод: [1, 42, 3]
    
  5. __iter__(self) и __next__(self): Позволяют классу поддерживать итерацию. __iter__ возвращает объект итератора, а __next__ возвращает следующий элемент.
    class Countdown:
        def __init__(self, start):
            self.start = start
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.start <= 0:
                raise StopIteration
            else:
                self.start -= 1
                return self.start + 1
    
    # Использование
    countdown = Countdown(5)
    for i in countdown:
        print(i)
    # Вывод:
    # 5
    # 4
    # 3
    # 2
    # 1
    
  1. __del__(self): Этот метод вызывается при удалении объекта. Однако, из-за неопределённости момента вызова и необходимости использовать сборщик мусора, его использование не рекомендуется, и вместо этого рекомендуется использовать метод __enter__ и __exit__ для управления ресурсами.
    class MyClass:
        def __del__(self):
            print("Этот объект уничтожен")
    
    # Использование
    obj = MyClass()
    del obj  # Вывод: "Этот объект уничтожен"
    
  1. __call__(self, ...): Позволяет вызывать экземпляр класса, как если бы он был функцией.
    class CallableClass:
        def __call__(self, x):
            return x * 2
    
    # Использование
    obj = CallableClass()
    result = obj(3)  # Вывод: 6
    
  1. __eq__(self, other): Определяет поведение оператора сравнения равенства (==).
    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __eq__(self, other):
            return self.x == other.x and self.y == other.y
    
    # Использование
    point1 = Point(1, 2)
    point2 = Point(1, 2)
    print(point1 == point2)  # Вывод: True
    
  1. __hash__(self): Возвращает хэш-значение объекта, используемое, например, при добавлении объектов в множество или использовании их в качестве ключей в словаре.
    class HashableClass:
        def __init__(self, value):
            self.value = value
    
        def __hash__(self):
            return hash(self.value)
    
    # Использование
    obj1 = HashableClass(42)
    obj2 = HashableClass(42)
    my_set = {obj1, obj2}
    print(len(my_set))  # Вывод: 1
    

Контекстный менеджер в Python

Контекстный менеджер в Python — это объект, который реализует методы __enter__ и __exit__, позволяя использовать его с оператором with.

Он обеспечивает управление кодом, когда требуются операции явного освобождения памяти или очистки.

Методы __enter__ и __exit__ предоставляют способ реализации управления ресурсами и контекстного менеджера в Python. Они используются в совместном с оператором with для обеспечения блока кода с определёнными начальными и конечными действиями. Это часто применяется для работы с ресурсами, такими как файлы или сетевые соединения, где важно обеспечить корректное освобождение ресурсов даже в случае возникновения исключений.

  1. Метод __enter__(self): Выполняется при входе в блок with. Возвращает значение, которое связывается с переменной после ключевого слова as в операторе with. Этот метод выполняет предварительную настройку и инициализацию.
  2. Метод __exit__(self, exc_type, exc_value, traceback): Выполняется при выходе из блока with. Принимает три аргумента, которые предоставляют информацию об исключении (если оно произошло). Если в блоке with не произошло исключение, то аргументы будут None. Этот метод используется для очистки и освобождения ресурсов.

Пример использования собственного контекстного менеджера с __enter__ и __exit__:

class MyContextManager:
    def __enter__(self):
        print("Входим в контекст")
        # Можно выполнить предварительные настройки, инициализацию и вернуть значение
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Выходим из контекста")
        # Можно выполнить очистку ресурсов здесь

# Использование контекстного менеджера
with MyContextManager() as cm:
    print("Внутри блока with")
    # Дополнительный код внутри блока with

# После выхода из блока with
print("За пределами блока with")

В приведенном примере метод __enter__ возвращает экземпляр класса MyContextManager. Это значение связывается с переменной cm, которую мы можем использовать внутри блока with. При выходе из блока with вызывается метод __exit__, который выполняет необходимую очистку.

Контекстные менеджеры и методы __enter__ и __exit__ часто используются при работе с файлами, блоками данных, базами данных и другими ресурсами, требующими управления жизненным циклом.

Примеры работы контекстных менеджеров

  1. Работа с файлами:
    # Без использования контекстного менеджера
    file = open('example.txt', 'r')
    content = file.read()
    file.close()
    
    # С использованием контекстного менеджера
    with open('example.txt', 'r') as file:
        content = file.read()
    

    Использование контекстного менеджера with open(...) as ... гарантирует закрытие файла после завершения блока кода.

  2. Работа с сетевыми соединениями:
    import socket
    
    # Без использования контекстного менеджера
    connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    connection.connect(('example.com', 80))
    data = connection.recv(1024)
    connection.close()
    
    # С использованием контекстного менеджера
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as connection:
        connection.connect(('example.com', 80))
        data = connection.recv(1024)
    

    Здесь контекстный менеджер обеспечивает корректное закрытие сокета.

  3. Управление транзакциями в базе данных:
    import sqlite3
    
    # Без использования контекстного менеджера
    connection = sqlite3.connect('my_database.db')
    cursor = connection.cursor()
    cursor.execute('SELECT * FROM my_table')
    data = cursor.fetchall()
    connection.commit()
    connection.close()
    
    # С использованием контекстного менеджера
    with sqlite3.connect('my_database.db') as connection:
        cursor = connection.cursor()
        cursor.execute('SELECT * FROM my_table')
        data = cursor.fetchall()
    # Автоматически выполняется commit и закрытие соединения
    

    В этом примере благодаря контекстному менеджеру with не нужно явно вызывать commit и close.

  4. Таймер выполнения кода:
    import time
    
    class Timer:
        def __enter__(self):
            self.start_time = time.time()
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            self.end_time = time.time()
            elapsed_time = self.end_time - self.start_time
            print(f"Время выполнения: {elapsed_time} секунд")
    
    # Использование таймера в контексте
    with Timer():
        # Ваш код, для которого измеряется время выполнения
        time.sleep(2)
    

    Этот пример демонстрирует, как можно использовать контекстный менеджер для измерения времени выполнения блока кода.

Защищенные и приватные методы для реализации инкапсуляции

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

  1. Атрибуты и методы с префиксом одного подчеркивания _:
    • Эти атрибуты и методы считаются «защищенными», что означает, что они не должны использоваться извне класса, но доступны для наследников.
    • Нет строгих правил в Python, и эта защита основана больше на соглашениях, чем на синтаксисе.

    Пример:

    class MyClass:
        def __init__(self):
            self._protected_attribute = 42
    
        def _protected_method(self):
            return "Этот метод защищен"
    
    obj = MyClass()
    print(obj._protected_attribute)      # Вывод: 42
    print(obj._protected_method())       # Вывод: Этот метод защищен
    
  2. Атрибуты и методы с префиксом двух подчеркиваний __:
    • Эти атрибуты и методы считаются «приватными» и не должны использоваться извне класса.
    • Имена приватных атрибутов и методов могут быть «манглированы» (изменены) интерпретатором Python для предотвращения конфликтов имен между классами.

    Пример:

    class MyClass:
        def __init__(self):
            self.__private_attribute = 42
    
        def __private_method(self):
            return "Этот метод приватен"
    
    obj = MyClass()
    # print(obj.__private_attribute)    # Ошибка: 'MyClass' object has no attribute '__private_attribute'
    # print(obj.__private_method())     # Ошибка: 'MyClass' object has no attribute '__private_method'
    print(obj._MyClass__private_attribute)  # Вывод: 42
    print(obj._MyClass__private_method())   # Вывод: Этот метод приватен
    

Используя подчеркивания в именах, вы можете указать другим программистам, какие атрибуты и методы предназначены для внутреннего использования в классе, а какие следует рассматривать как внутренние детали, доступные только изнутри класса.

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

  • Инкапсуляция в Python основана на соглашениях и хороших практиках, а не на полном ограничении доступа.
  • Даже приватные атрибуты могут быть доступны извне, если у пользователя есть к ним явный доступ.
  • Имена приватных атрибутов манглируются для предотвращения случайного пересечения имен в подклассах.

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

Геттер, сеттер и делитер. Способы работы с защищенными и приватными методами и атрибутами.

Геттер, сеттер и делитер — это термины, связанные с использованием свойств (properties) в объектно-ориентированном программировании. Они предоставляют интерфейс для доступа, установки и удаления значений атрибутов объекта. В языке программирования Python геттеры, сеттеры и делитеры могут быть определены с использованием декораторов @property, @<attribute_name>.setter и @<attribute_name>.deleter.

  1. Геттер (Getter):
    • Геттер — это метод, который используется для получения значения атрибута объекта. В Python геттеры могут быть созданы с использованием декоратора @property. Они позволяют обращаться к атрибуту, как если бы это был атрибут класса, а не метод.
      class MyClass:
          def __init__(self):
              self._my_attribute = 42
      
          @property
          def my_attribute(self):
              return self._my_attribute
      
      obj = MyClass()
      print(obj.my_attribute)  # Вывод: 42
      
  2. Сеттер (Setter):
    • Сеттер — это метод, который используется для установки значения атрибута объекта. Сеттеры в Python могут быть созданы с использованием декоратора @<attribute_name>.setter. Они вызываются при попытке установить значение атрибута.
      class MyClass:
          def __init__(self):
              self._my_attribute = 0
      
          @property
          def my_attribute(self):
              return self._my_attribute
      
          @my_attribute.setter
          def my_attribute(self, value):
              if value > 0:
                  self._my_attribute = value
      
      obj = MyClass()
      obj.my_attribute = 42
      print(obj.my_attribute)  # Вывод: 42
      
      obj.my_attribute = -5
      print(obj.my_attribute)  # Вывод: 42, так как сеттер не позволит установить отрицательное значение
      
  3. Делитер (Deleter):
    • Делитер — это метод, который используется для удаления значения атрибута объекта. Делитеры могут быть созданы с использованием декоратора @<attribute_name>.deleter.
      class MyClass:
          def __init__(self):
              self._my_attribute = 42
      
          @property
          def my_attribute(self):
              return self._my_attribute
      
          @my_attribute.deleter
          def my_attribute(self):
              print(f"Deleting my_attribute with value {self._my_attribute}")
              del self._my_attribute
      
      obj = MyClass()
      del obj.my_attribute  # Вывод: Deleting my_attribute with value 42
      # print(obj.my_attribute)  # Ошибка: 'MyClass' object has no attribute '_my_attribute'
      

В Python существуют различные средства поддержки геттеров и сеттеров для защищенных (protected) и приватных (private) атрибутов класса.

  1. Декораторы property, @property, @<attribute_name>.setter:
    • property — это встроенный декоратор, который позволяет создавать геттеры, сеттеры и делитеры для атрибутов класса.
      class MyClass:
          def __init__(self):
              self._protected_attribute = None  # защищенный атрибут
      
          @property
          def protected_attribute(self):
              return self._protected_attribute
      
          @protected_attribute.setter
          def protected_attribute(self, value):
              if value > 0:
                  self._protected_attribute = value
      
      obj = MyClass()
      obj.protected_attribute = 42
      print(obj.protected_attribute)  # Вывод: 42
      
  2. Декоратор @property для приватных атрибутов:
    • Аналогично можно использовать property и для приватных атрибутов.
      class MyClass:
          def __init__(self):
              self.__private_attribute = None  # приватный атрибут
      
          @property
          def private_attribute(self):
              return self.__private_attribute
      
          @private_attribute.setter
          def private_attribute(self, value):
              if value > 0:
                  self.__private_attribute = value
      
      obj = MyClass()
      obj.private_attribute = 42
      print(obj.private_attribute)  # Вывод: 42
      
  3. Использование декоратора @property без setter для создания read-only атрибутов:
    • Если вы хотите создать атрибут только для чтения (read-only), вы можете использовать только геттер.
      class MyClass:
          def __init__(self):
              self.__read_only_attribute = 42  # приватный, только для чтения
      
          @property
          def read_only_attribute(self):
              return self.__read_only_attribute
      
      obj = MyClass()
      print(obj.read_only_attribute)  # Вывод: 42
      # obj.read_only_attribute = 10  # Ошибка: can't set attribute
      
  4. Использование декоратора @property для создания computed атрибутов:
    • Вы можете использовать property для создания атрибутов, значения которых вычисляются на лету.
      class Circle:
          def __init__(self, radius):
              self._radius = radius
      
          @property
          def diameter(self):
              return 2 * self._radius
      
      circle = Circle(5)
      print(circle.diameter)  # Вывод: 10
      

Статические методы и атрибуты

Атрибуты и методы, которые можно вызывать только напрямую из класса, а не из его экземпляров, называются статическими (static) атрибутами и методами.

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

Статические методы определяются с использованием декоратора @staticmethod или ключевого слова staticmethod. Они не требуют доступа к экземпляру и не могут использовать self. Пример:

class MathOperations:
    @staticmethod
    def add(x, y):
        return x + y

# Вызов статического метода напрямую из класса
result = MathOperations.add(3, 5)
print(result)  # Вывод: 8

Статические атрибуты просто определяются внутри класса и используются через имя класса, а не через экземпляр. Пример:

class Configuration:
    max_connections = 10

# Доступ к статическому атрибуту напрямую из класса
print(Configuration.max_connections)  # Вывод: 10

Зачем нужны статические методы и атрибуты?

  1. Общие операции, не зависящие от конкретных экземпляров: Если у вас есть метод, который выполняет операцию, не зависящую от состояния конкретного объекта, это может быть хорошим кандидатом для статического метода. Это может сделать код более чистым и понятным.
  2. Совместное использование данных между экземплярами: Статические атрибуты могут использоваться для общего хранения данных между всеми экземплярами класса. Например, счетчик экземпляров:
    class MyClass:
        instance_count = 0
    
        def __init__(self):
            MyClass.instance_count += 1
    

Реализация абстракции через ABC

ABC, или Abstract Base Classes (абстрактные базовые классы), представляют собой механизм в Python для создания абстрактных классов. Абстрактный класс — это класс, который не предназначен для создания экземпляров, а служит в основном для определения интерфейса, который должны реализовать его подклассы. ABC в Python реализуется с использованием модуля abc и декораторов.

Декораторы, используемые при создании абстрактных классов:

  1. @abstractmethod: Этот декоратор используется для указания абстрактного метода в абстрактном классе. Абстрактный метод — это метод, который должен быть реализован в подклассах. Если подкласс не реализует абстрактный метод, то он сам становится абстрактным и не может быть использован.
    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 * self.radius
    
  2. @abstractproperty: Этот декоратор используется для определения абстрактного свойства в абстрактном классе. Свойства в Python представляют собой методы, которые можно вызвать без использования скобок. Свойства, помеченные @abstractproperty, должны быть реализованы в подклассах.
    from abc import ABC, abstractproperty
    
    class Vehicle(ABC):
        @abstractproperty
        def speed(self):
            pass
    
    class Car(Vehicle):
        def __init__(self, speed):
            self._speed = speed
    
        @property
        def speed(self):
            return self._speed
    
  3. @abstractclassmethod и @abstractstaticmethod: Подобные декораторы используются для создания абстрактных методов классов и статических методов соответственно. Они действуют аналогично @abstractmethod, но применяются к методам класса и статическим методам.
    from abc import ABC, abstractclassmethod, abstractstaticmethod
    
    class DatabaseConnector(ABC):
        @abstractclassmethod
        def connect(cls, credentials):
            pass
    
        @abstractstaticmethod
        def version():
            pass

Перечисляемые типы

Enum (перечисление) в Python — это тип данных, который предоставляет способ создания и использования именованных констант. Использование Enum может сделать ваш код более читаемым, поддерживаемым и предотвратить ошибки из-за опечаток или неправильных значений.

Вот несколько причин, почему использование Enum может быть полезным:

  1. Читаемость кода:
    • Именованные константы, предоставляемые Enum, делают код более ясным и самодокументирующимся.
    • Вместо использования магических чисел или строк, вы можете использовать более осмысленные имена.
      from enum import Enum
      
      class Weekday(Enum):
          MONDAY = 1
          TUESDAY = 2
          WEDNESDAY = 3
          THURSDAY = 4
          FRIDAY = 5
          SATURDAY = 6
          SUNDAY = 7
      
  2. Безопасность и проверка типов:
    • Enum предотвращает использование неправильных значений, так как только те значения, которые указаны в перечислении, считаются допустимыми.
    • Это помогает избежать ошибок, связанных с опечатками и неправильными значениями.
      day = Weekday.MONDAY
      # day = Weekday.WRONG_DAY  # Ошибка: 'Weekday' has no member 'WRONG_DAY'
      
  3. Сопоставление значений:
    • Вы можете сравнивать значения перечислений, что делает код более выразительным и легким для понимания.
      today = Weekday.MONDAY
      
      if today == Weekday.MONDAY:
          print("It's Monday!")
      
  4. Итерация по значениям:
    • Вы можете легко итерироваться по всем значениям в перечислении, что может быть удобным в некоторых сценариях.
      for day in Weekday:
          print(day)
      
  5. Ограничение выбора:
    • Перечисления могут использоваться для явного ограничения выбора значений в определенном контексте.
      def set_weekday(day):
          if day in Weekday:
              print(f"Setting the weekday to {day}")
          else:
              print("Invalid weekday")
      
      set_weekday(Weekday.MONDAY)
      set_weekday("InvalidDay")  # Вывод: Invalid weekday
      
  6. Читаемый вывод:
    • Значениями перечисления можно обращаться, как к строкам, но они всегда будут сравниваться по их уникальным именам, что делает вывод более читаемым.
      print(Weekday.MONDAY)  # Вывод: Weekday.MONDAY
      

Классы для хранения данных. Декоратор @dataclass

В Python, декоратор dataclass представляет собой специальный механизм для автоматического создания методов, обычно связанных с обработкой данных, в классах, предназначенных для хранения данных. Декоратор dataclass добавляет стандартные методы, такие как __init__, __repr__, и другие, что делает класс более удобным для использования в сценариях, где основной целью является хранение и обработка данных.

Несколько причин, по которым можно использовать декоратор dataclass:

  1. Уменьшение объема кода:
    • Декоратор dataclass автоматически генерирует стандартные методы, такие как __init__ и __repr__, что уменьшает объем шаблонного кода, который обычно нужно написать для создания классов для хранения данных.
  2. Явное указание полей данных:
    • Декоратор позволяет явно указать поля данных, что делает код более ясным и обеспечивает лучшую документацию для кода.
      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: float
          y: float
      
  3. Неизменяемость (immutable) по умолчанию:
    • По умолчанию, экземпляры классов, созданные с использованием dataclass, являются неизменяемыми. То есть, они не могут быть изменены после создания.
      from dataclasses import dataclass
      
      @dataclass(frozen=True)
      class Point:
          x: float
          y: float
      
  4. Поддержка сравнения:
    • Декоратор dataclass автоматически добавляет методы для сравнения экземпляров класса, что упрощает сравнение объектов на основе их данных.
      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: float
          y: float
      
      p1 = Point(1.0, 2.0)
      p2 = Point(1.0, 2.0)
      
      print(p1 == p2)  # Вывод: True
      
  5. Поддержка хеширования:
    • Если все поля данных класса являются неизменяемыми, декоратор dataclass также автоматически добавляет поддержку хеширования.
      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: float
          y: float
      
      p = Point(1.0, 2.0)
      hash_value = hash(p)
      
  6. Поддержка десериализации (через модуль dataclasses_json, например):
    • Декоратор dataclass упрощает сериализацию и десериализацию объектов данных.
      from dataclasses import dataclass
      from dataclasses_json import dataclass_json
      
      @dataclass_json
      @dataclass
      class Point:
          x: float
          y: float
      
      p = Point(1.0, 2.0)
      
      # Сериализация в JSON
      json_data = p.to_json()
      # Вывод: '{"x": 1.0, "y": 2.0}'
      
      # Десериализация из JSON
      new_point = Point.from_json(json_data)
      

Задания для подготовки

Задание 1. Создайте классы для представления различных геометрических тел, таких как сфера, параллелепипед, конус и т.д. Используйте принцип наследования для создания родительского класса, который будет содержать общие атрибуты и методы для всех тел, например, объем и площадь поверхности. Затем создайте дочерние классы, которые наследуют эти атрибуты и методы и добавляют дополнительные, специфичные для каждого тела.

Решение:

Для решения данной задачи можно создать базовый класс GeometricSolid со следующими общими атрибутами и методами:

  • volume() — метод для вычисления объема тела
  • surface_area() — метод для вычисления площади поверхности тела

Затем можно создать дочерние классы, которые будут наследовать от GeometricSolid и добавлять свои собственные атрибуты и методы для конкретных геометрических тел.

Пример реализации:

import math

class GeometricSolid:
    def volume(self):
        pass
    
    def surface_area(self):
        pass

class Sphere(GeometricSolid):
    def __init__(self, radius):
        self.radius = radius
        
    def volume(self):
        return 4/3 * math.pi * self.radius**3
    
    def surface_area(self):
        return 4 * math.pi * self.radius**2

class Cube(GeometricSolid):
    def __init__(self, length):
        self.length = length
        
    def volume(self):
        return self.length**3
    
    def surface_area(self):
        return 6 * self.length**2

class Cylinder(GeometricSolid):
    def __init__(self, radius, height):
        self.radius = radius
        self.height = height
        
    def volume(self):
        return math.pi * self.radius**2 * self.height
    
    def surface_area(self):
        return 2 * math.pi * self.radius * self.height + 2 * math.pi * self.radius**2

В этом примере мы создали три дочерних класса: Sphere, Cube, и Cylinder. В каждом из них мы определили конструктор, который принимает необходимые аргументы для вычисления объема и площади поверхности. Затем мы переопределили методы volume() и surface_area() для каждого класса, чтобы они возвращали значения, соответствующие конкретному геометрическому телу.

Пример использования:

sphere = Sphere(5)
print("Sphere radius:", sphere.radius)
print("Sphere volume:", sphere.volume())
print("Sphere surface area:", sphere.surface_area())

cube = Cube(3)
print("Cube length:", cube.length)
print("Cube volume:", cube.volume())
print("Cube surface area:", cube.surface_area())

cylinder = Cylinder(2, 5)
print("Cylinder radius:", cylinder.radius)
print("Cylinder height:", cylinder.height)
print("Cylinder volume:", cylinder.volume())
print("Cylinder surface area:", cylinder.surface_area())

Результат выполнения:

Sphere radius: 5
Sphere volume: 523.5987755982989
Sphere surface area: 314.1592653589793
Cube length: 3
Cube volume: 27
Cube surface area: 54
Cylinder radius: 2
Cylinder height: 5
Cylinder volume: 62.83185307179586
Cylinder surface area: 62.83185307179586

Задание 2. Создайте классы для представления различных животных, например, кошек, собак, птиц и т.д. Используйте принцип наследования для создания родительского класса, который будет содержать общие атрибуты и методы для всех животных, например, возраст и цвет шерсти. Затем создайте дочерние классы, которые наследуют эти атрибуты и методы и добавляют дополнительные, специфичные для каждого вида животных, например, способность летать или область обитания.

Решение:
class Animal:
    def __init__(self, age, color):
        self.age = age
        self.color = color
    
    def speak(self):
        print("I am an animal")
        

class Cat(Animal):
    def __init__(self, age, color, breed):
        super().__init__(age, color)
        self.breed = breed
    
    def speak(self):
        print("Meow")
        

class Dog(Animal):
    def __init__(self, age, color, breed):
        super().__init__(age, color)
        self.breed = breed
    
    def speak(self):
        print("Woof")
        

class Bird(Animal):
    def __init__(self, age, color, wingspan):
        super().__init__(age, color)
        self.wingspan = wingspan
    
    def speak(self):
        print("Chirp")
        

cat = Cat(2, "black", "siamese")
dog = Dog(4, "brown", "golden retriever")
bird = Bird(1, "green", 12)

print(cat.age)         # Output: 2
print(dog.breed)       # Output: golden retriever
print(bird.color)      # Output: green

cat.speak()            # Output: Meow
dog.speak()            # Output: Woof
bird.speak()           # Output: Chirp

В этом примере мы создали базовый класс Animal, который содержит общие атрибуты и методы для всех животных, такие как возраст и цвет шерсти, а также метод speak, который печатает звук, который производит животное. Затем мы создали три дочерних класса: Cat, Dog и Bird, которые наследуют от Animal и добавляют свои собственные атрибуты и методы, например, порода для кошек и собак и размах крыльев для птиц. Каждый класс также переопределяет метод speak, чтобы выводить свой собственный звук.

В конце мы создали объекты каждого класса и вызвали их методы и атрибуты.

Задание 3. Создайте классы для представления различных транспортных средств, например, автомобилей, велосипедов, самолетов и т.д. Используйте принцип наследования для создания родительского класса, который будет содержать общие атрибуты и методы для всех транспортных средств, например, скорость и вместимость. Затем создайте дочерние классы, которые наследуют эти атрибуты и методы и добавляют дополнительные, специфичные для каждого транспортного средства, например, тип двигателя или способность летать.

Решение:
class Transport:
    def __init__(self, speed, capacity):
        self.speed = speed
        self.capacity = capacity

    def move(self):
        print(f"Moving at {self.speed} km/h")

    def load(self, amount):
        if amount <= self.capacity:
            print(f"Loading {amount} passengers")
        else:
            print(f"Not enough capacity to load {amount} passengers")


class Car(Transport):
    def __init__(self, speed, capacity, engine_type):
        super().__init__(speed, capacity)
        self.engine_type = engine_type

    def park(self):
        print("Parking the car")

    def honk(self):
        print("Honking the horn")


class Bicycle(Transport):
    def __init__(self, speed, capacity, frame_material):
        super().__init__(speed, capacity)
        self.frame_material = frame_material

    def ride(self):
        print("Riding the bicycle")

    def ring_bell(self):
        print("Ringing the bell")


class Airplane(Transport):
    def __init__(self, speed, capacity, engine_type, wingspan):
        super().__init__(speed, capacity)
        self.engine_type = engine_type
        self.wingspan = wingspan

    def fly(self):
        print("Flying the airplane")

    def land(self):
        print("Landing the airplane")

Здесь класс Transport является родительским классом для всех транспортных средств и содержит общие атрибуты и методы для них, такие как скорость и вместимость.

Затем мы создали три дочерних класса: Car, Bicycle и Airplane. Класс Car добавляет атрибут engine_type и методы park и honk. Класс Bicycle добавляет атрибут frame_material и методы ride и ring_bell. Класс Airplane добавляет атрибуты engine_type и wingspan и методы fly и land.

Теперь мы можем создавать объекты каждого класса и вызывать их методы, как показано ниже:

car = Car(100, 5, "gasoline")
car.move() # Moving at 100 km/h
car.load(3) # Loading 3 passengers
car.park() # Parking the car
car.honk() # Honking the horn

bicycle = Bicycle(20, 1, "steel")
bicycle.move() # Moving at 20 km/h
bicycle.load(1) # Loading 1 passengers
bicycle.ride() # Riding the bicycle
bicycle.ring_bell() # Ringing the bell

airplane = Airplane(800, 200, "jet", 40)
airplane.move() # Moving at 800 km/h
airplane.load(150) # Loading 150 passengers
airplane.fly() # Flying the airplane
airplane.land() # Landing the airplane

Задание 4. Создайте классы для представления различных продуктов в магазине, например, книг, музыки, электроники и т.д. Используйте принцип наследования для создания родительского класса, который будет содержать общие атрибуты и методы для всех продуктов, например, цена и описание. Затем создайте дочерние классы, которые наследуют эти атрибуты и методы и добавляют дополнительные, специфичные для каждого продукта, например, автора или продолжительность воспроизведения.

Решение:
class Product:
    def __init__(self, name, price, description):
        self.name = name
        self.price = price
        self.description = description
        
    def display_info(self):
        print(f"{self.name} - {self.price} руб. ({self.description})")


class Book(Product):
    def __init__(self, name, price, description, author, pages):
        super().__init__(name, price, description)
        self.author = author
        self.pages = pages
        
    def display_info(self):
        super().display_info()
        print(f"Автор: {self.author}, Количество страниц: {self.pages}")


class Music(Product):
    def __init__(self, name, price, description, artist, duration):
        super().__init__(name, price, description)
        self.artist = artist
        self.duration = duration
        
    def display_info(self):
        super().display_info()
        print(f"Исполнитель: {self.artist}, Продолжительность: {self.duration}")


class Electronics(Product):
    def __init__(self, name, price, description, manufacturer, power):
        super().__init__(name, price, description)
        self.manufacturer = manufacturer
        self.power = power
        
    def display_info(self):
        super().display_info()
        print(f"Производитель: {self.manufacturer}, Мощность: {self.power} Вт")

В этом примере мы создали родительский класс Product, который содержит общие атрибуты и методы для всех продуктов. Затем мы создали дочерние классы Book, Music и Electronics, которые наследуют эти атрибуты и методы, а также добавляют специфичные для каждого продукта атрибуты и методы.

Каждый дочерний класс имеет свой собственный метод display_info, который выводит информацию о продукте, включая дополнительные атрибуты, специфичные для этого продукта.

Например, мы можем создать экземпляры классов и вызвать их методы display_info:

book = Book("Война и мир", 1500, "Роман Л. Н. Толстого", "Лев Толстой", 1225)
book.display_info()

music = Music("Stairway to Heaven", 99, "Культовая песня группы Led Zeppelin", "Led Zeppelin", "8:02")
music.display_info()

electronics = Electronics("Samsung Galaxy S21", 69990, "Смартфон", "Samsung", 25)
electronics.display_info()

Результат

Война и мир - 1500 руб. (Роман Л. Н. Толстого)
Автор: Лев Толстой, Количество страниц: 1225
Stairway to Heaven - 99 руб. (Культовая песня группы Led Zeppelin)
Исполнитель: Led Zeppelin, Продолжительность: 8:02
Samsung Galaxy S21 - 69990 руб. (Смартфон)
Производитель: Samsung

 

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.