Как написать свой класс python

Данный урок посвящен объектно-ориентированному программированию в Python. Разобраны такие темы как создание объектов и классов, работа с конструктором, наследование и полиморфизм в Python.

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

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

Выделяют три основных “столпа” ООП- это инкапсуляция, наследование и полиморфизм.

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

Под инкапсуляцией понимается сокрытие деталей реализации, данных и т.п. от внешней стороны. Например, можно определить класс “холодильник”, который будет содержать следующие данные: производитель, объем, количество камер хранения, потребляемая мощность и т.п., и методы: открыть/закрыть холодильник, включить/выключить, но при этом реализация того, как происходит непосредственно включение и выключение пользователю вашего класса не доступна, что позволяет ее менять без опасения, что это может отразиться на использующей класс «холодильник» программе. При этом класс становится новым типом данных в рамках разрабатываемой программы. Можно создавать переменные этого нового типа, такие переменные называются объекты.

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

Под наследованием понимается возможность создания нового класса на базе существующего. Наследование предполагает наличие отношения “является” между классом наследником и классом родителем. При этом класс потомок будет содержать те же атрибуты и методы, что и базовый класс, но при этом его можно (и нужно) расширять через добавление новых методов и атрибутов.

Примером базового класса, демонстрирующего наследование, можно определить класс “автомобиль”, имеющий атрибуты: масса, мощность двигателя, объем топливного бака и методы: завести и заглушить. У такого класса может быть потомок – “грузовой автомобиль”, он будет содержать те же атрибуты и методы, что и класс “автомобиль”, и дополнительные свойства: количество осей, мощность компрессора и т.п..

Полиморфизм

Полиморфизм позволяет одинаково обращаться с объектами, имеющими однотипный интерфейс, независимо от внутренней реализации объекта. Например, с объектом класса “грузовой автомобиль” можно производить те же операции, что и с объектом класса “автомобиль”, т.к. первый является наследником второго, при этом обратное утверждение неверно (во всяком случае не всегда). Другими словами полиморфизм предполагает разную реализацию методов с одинаковыми именами. Это очень полезно при наследовании, когда в классе наследнике можно переопределить методы класса родителя.

Создание классов и объектов

Создание класса в Python начинается с инструкции class. Вот так будет выглядеть минимальный класс.

class C: 
    pass

Класс состоит из объявления (инструкция class), имени класса (нашем случае это имя C) и тела класса, которое содержит атрибуты и методы (в нашем минимальном классе есть только одна инструкция pass).

Для того чтобы создать объект класса необходимо воспользоваться следующим синтаксисом:

имя_объекта = имя_класса()

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

Как уже было сказано выше, класс может содержать атрибуты и методы. Атрибут может быть статическим и динамическим (уровня объекта класса). Суть в том, что для работы со статическим атрибутом, вам не нужно создавать экземпляр класса, а для работы с динамическим – нужно. Пример:

class Rectangle:
    default_color = "green"

    def __init__(self, width, height):
        self.width = width
        self.height = height

В представленном выше классе, атрибут default_color – это статический атрибут, и доступ к нему, как было сказано выше, можно получить не создавая объект класса Rectangle.

>>> Rectangle.default_color
'green'

width и height – это динамические атрибуты, при их создании было использовано ключевое слово self. Пока просто примите это как должное, более подробно про self будет рассказано ниже. Для доступа к width и height предварительно нужно создать объект класса Rectangle:

>>> rect = Rectangle(10, 20)
>>> rect.width
10
>>> rect.height
20

Если обратиться через класс, то получим ошибку:

>>> Rectangle.width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Rectangle' has no attribute 'width'

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

Проверим ещё раз значение атрибута default_color:

>>> Rectangle.default_color
'green'

Присвоим ему новое значение:

>>> Rectangle.default_color = "red"
>>> Rectangle.default_color
'red'

Создадим два объекта класса Rectangle и проверим, что default_color у них совпадает:

>>> r1 = Rectangle(1,2)
>>> r2 = Rectangle(10, 20)
>>> r1.default_color
'red'
>>> r2.default_color
'red'

Если поменять значение default_color через имя класса Rectangle, то все будет ожидаемо: у объектов r1 и r2 это значение изменится, но если поменять его через экземпляр класса, то у экземпляра будет создан атрибут с таким же именем как статический, а доступ к последнему будет потерян:

Меняем default_color через r1:

>>> r1.default_color = "blue"
>>> r1.default_color
'blue'

При этом у r2 остается значение статического атрибута:

>>> r2.default_color
'red'
>>> Rectangle.default_color
'red'

Вообще напрямую работать с атрибутами – не очень хорошая идея, лучше для этого использовать свойства.

Методы класса

Добавим к нашему классу метод. Метод – это функция, находящаяся внутри класса и выполняющая определенную работу.

Методы бывают статическими, классовыми (среднее между статическими и обычными) и уровня класса (будем их называть просто словом метод). Статический метод создается с декоратором @staticmethod, классовый – с декоратором @classmethod, первым аргументом в него передается cls, обычный метод создается без специального декоратора, ему первым аргументом передается self:

class MyClass:

    @staticmethod
    def ex_static_method():
        print("static method")

    @classmethod
    def ex_class_method(cls):
        print("class method")

    def ex_method(self):
        print("method")

Статический и классовый метод можно вызвать, не создавая экземпляр класса, для вызова ex_method() нужен объект:

>>> MyClass.ex_static_method()
static method

>>> MyClass.ex_class_method()
class method

>>> MyClass.ex_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ex_method() missing 1 required positional argument: 'self'

>>> m = MyClass()
>>> m.ex_method()
method

Конструктор класса и инициализация экземпляра класса

В Python разделяют конструктор класса и метод для инициализации экземпляра класса. Конструктор класса это метод __new__(cls, *args, **kwargs) для инициализации экземпляра класса используется метод __init__(self). При этом, как вы могли заметить __new__ – это классовый метод, а __init__ таким не является. Метод __new__ редко переопределяется, чаще используется реализация от базового класса object (см. раздел Наследование), __init__ же наоборот является очень удобным способом задать параметры объекта при его создании.

Создадим реализацию класса Rectangle с измененным конструктором и инициализатором, через который задается ширина и высота прямоугольника:

class Rectangle:

    def __new__(cls, *args, **kwargs):
        print("Hello from __new__")
        return super().__new__(cls)

    def __init__(self, width, height):
        print("Hello from __init__")
        self.width = width
        self.height = height


>>> rect = Rectangle(10, 20)
Hello from __new__
Hello from __init__

>>> rect.width
10

>>> rect.height
20

Что такое self?

До этого момента вы уже успели познакомиться с ключевым словом self. self – это ссылка на текущий экземпляр класса, в таких языках как Java, C# аналогом является ключевое слово this. Через self вы получаете доступ к атрибутам и методам класса внутри него:

class Rectangle:

    def __init__(self, width, height):
        self.width = width
        self.height = height

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

В приведенной реализации метод area получает доступ к атрибутам width и height для расчета площади. Если бы в качестве первого параметра не было указано self, то при попытке вызвать area программа была бы остановлена с ошибкой.

Уровни доступа атрибута и метода

Если вы знакомы с языками программирования Java, C#, C++ то, наверное, уже задались вопросом: “а как управлять уровнем доступа?”. В перечисленных языка вы можете явно указать для переменной, что доступ к ней снаружи класса запрещен, это делается с помощью ключевых слов (private, protected и т.д.). В Python таких возможностей нет, и любой может обратиться к атрибутам и методам вашего класса, если возникнет такая необходимость. Это существенный недостаток этого языка, т.к. нарушается один из ключевых принципов ООП – инкапсуляция. Хорошим тоном считается, что для чтения/изменения какого-то атрибута должны использоваться специальные методы, которые называются getter/setter, их можно реализовать, но ничего не помешает изменить атрибут напрямую. При этом есть соглашение, что метод или атрибут, который начинается с нижнего подчеркивания, является скрытым, и снаружи класса трогать его не нужно (хотя сделать это можно).

Внесем соответствующие изменения в класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self._width = width
        self._height = height

    def get_width(self):
        return self._width

    def set_width(self, w):
        self._width = w

    def get_height(self):
        return self._height

    def set_height(self, h):
        self._height = h

    def area(self):
        return self._width * self._height

В приведенном примере для доступа к _width и _height используются специальные методы, но ничего не мешает вам обратиться к ним (атрибутам) напрямую.

>>> rect = Rectangle(10, 20)

>>> rect.get_width()
10

>>> rect._width
10

Если же атрибут или метод начинается с двух подчеркиваний, то тут напрямую вы к нему уже не обратитесь (простым образом). Модифицируем наш класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    def get_width(self):
        return self.__width

    def set_width(self, w):
        self.__width = w

    def get_height(self):
        return self.__height

    def set_height(self, h):
        self.__height = h

    def area(self):
        return self.__width * self.__height

Попытка обратиться к __width напрямую вызовет ошибку, нужно работать только через get_width():

>>> rect = Rectangle(10, 20)

>>> rect.__width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__width'

>>> rect.get_width()
10

Но на самом деле это сделать можно, просто этот атрибут теперь для внешнего использования носит название: _Rectangle__width:

>>> rect._Rectangle__width
10

>>> rect._Rectangle__width = 20

>>> rect.get_width()
20

Свойства

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

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

Сделаем реализацию класса Rectangle с использованием свойств:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def area(self):
        return self.__width * self.__height

Теперь работать с width и height можно так, как будто они являются атрибутами:

>>> rect = Rectangle(10, 20)

>>> rect.width
10

>>> rect.height
20

Можно не только читать, но и задавать новые значения свойствам:

>>> rect.width = 50

>>> rect.width
50

>>> rect.height = 70

>>> rect.height
70

Если вы обратили внимание: в setter’ах этих свойств осуществляется проверка входных значений, если значение меньше нуля, то будет выброшено исключение ValueError:

>>> rect.width = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 28, in width
    raise ValueError
ValueError

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

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

Синтаксически создание класса с указанием его родителя выглядит так:

class имя_класса(имя_родителя1, [имя_родителя2,…, имя_родителя_n])

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

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c


class Rectangle(Figure): 

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError 

    def area(self):
        return self.__width * self.__height

Родительским классом является Figure, который при инициализации принимает цвет фигуры и предоставляет его через свойства. Rectangle – класс наследник от Figure. Обратите внимание на его метод __init__: в нем первым делом вызывается конструктор (хотя это не совсем верно, но будем говорить так) его родительского класса:

super().__init__(color)

super – это ключевое слово, которое используется для обращения к родительскому классу.

Теперь у объекта класса Rectangle помимо уже знакомых свойств width и height появилось свойство color:

>>> rect = Rectangle(10, 20, "green")

>>> rect.width
10

>>> rect.height
20

>>> rect.color
'green'

>>> rect.color = "red"

>>> rect.color
'red'

Полиморфизм

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

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c

    def info(self):
       print("Figure")
       print("Color: " + self.__color)


class Rectangle(Figure):

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def info(self):
        print("Rectangle")
        print("Color: " + self.color)
        print("Width: " + str(self.width))
        print("Height: " + str(self.height))
        print("Area: " + str(self.area()))

    def area(self):
        return self.__width * self.__height

Посмотрим, как это работает

>>> fig = Figure("orange")

>>> fig.info()
Figure
Color: orange

>>> rect = Rectangle(10, 20, "green")

>>> rect.info()
Rectangle
Color: green
Width: 10
Height: 20
Area: 200

Таким образом, класс наследник может расширять функционал класса родителя.

P.S.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

<<< Python. Урок 13. Модули и пакеты   Python. Урок 15. Итераторы и генераторы>>>

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

Создание классов

Оператор class создает новое определение класса. Имя класса сразу следует за ключевым словом class, после которого ставиться двоеточие:

class ClassName:
   """Необязательная строка документации класса"""  
   class_suite
  • У класса есть строка документации, к которой можно получить доступ через ClassName.__doc__.
  • class_suite состоит из частей класса, атрибутов данных и функции.

Пример создания класса на Python:

class Employee:  
    """Базовый класс для всех сотрудников"""  
    emp_count = 0  
  
    def __init__(self, name, salary):  
        self.name = name  
        self.salary = salary  
        Employee.emp_count += 1  
  
    def display_count(self):  
        print('Всего сотрудников: %d' % Employee.empCount)  
  
    def display_employee(self):  
        print('Имя: {}. Зарплата: {}'.format(self.name, self.salary))

  • Переменная emp_count — переменная класса, значение которой разделяется между экземплярами этого класса. Получить доступ к этой переменной можно через Employee.emp_count из класса или за его пределами.
  • Первый метод __init__() — специальный метод, который называют конструктором класса или методом инициализации. Его вызывает Python при создании нового экземпляра этого класса.
  • Объявляйте другие методы класса, как обычные функции, за исключением того, что первый аргумент для каждого метода self. Python добавляет аргумент self в список для вас; и тогда вам не нужно включать его при вызове этих методов.

Создание экземпляров класса

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

# Это создаст первый объект класса Employee  
emp1 = Employee("Андрей", 2000)  
# Это создаст второй объект класса Employee  
emp2 = Employee("Мария", 5000) 

Доступ к атрибутам

Получите доступ к атрибутам класса, используя оператор . после объекта класса. Доступ к классу можно получить используя имя переменой класса:

emp1.display_employee()  
emp2.display_employee()  
print("Всего сотрудников: %d" % Employee.emp_count)

Теперь, систематизируем все.

class Employee:  
    """Базовый класс для всех сотрудников"""  
    emp_count = 0  
  
    def __init__(self, name, salary):  
        self.name = name  
        self.salary = salary  
        Employee.emp_count += 1  
  
    def display_count(self):  
        print('Всего сотрудников: %d' % Employee.emp_count)
        
    def display_employee(self):  
        print('Имя: {}. Зарплата: {}'.format(self.name, self.salary))  
  
  
# Это создаст первый объект класса Employee  
emp1 = Employee("Андрей", 2000)  
# Это создаст второй объект класса Employee  
emp2 = Employee("Мария", 5000)  
emp1.display_employee()  
emp2.display_employee()  
print("Всего сотрудников: %d" % Employee.emp_count)

При выполнении этого кода, мы получаем следующий результат:

Имя: Андрей. Зарплата: 2000
Имя: Мария. Зарплата: 5000
Всего сотрудников: 2

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

emp1.age = 7  # Добавит атрибут 'age'
emp1.age = 8  # Изменит атрибут 'age'
del emp1.age  # Удалит атрибут 'age'

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

  • getattr(obj, name [, default]) — для доступа к атрибуту объекта.
  • hasattr(obj, name) — проверить, есть ли в obj атрибут name.
  • setattr(obj, name, value) — задать атрибут. Если атрибут не существует, он будет создан.
  • delattr(obj, name) — удалить атрибут.
hasattr(emp1, 'age')  # возвращает true если атрибут 'age' существует
getattr(emp1, 'age')  # возвращает значение атрибута 'age' 
setattr(emp1, 'age', 8)  #устанавливает атрибут 'age' на 8
delattr(empl, 'age')  # удаляет атрибут 'age'

Встроенные атрибуты класса

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

  • __dict__ — словарь, содержащий пространство имен класса.
  • __doc__ — строка документации класса. None если, документация отсутствует.
  • __name__ — имя класса.
  • __module__ — имя модуля, в котором определяется класс. Этот атрибут __main__ в интерактивном режиме.
  • __bases__ — могут быть пустые tuple, содержащие базовые классы, в порядке их появления в списке базового класса.

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

class Employee:  
    """Базовый класс для всех сотрудников"""  
    emp_count = 0  
  
    def __init__(self, name, salary):  
        self.name = name  
        self.salary = salary  
        Employee.empCount += 1  
  
    def display_count(self):  
        print('Всего сотрудников: %d' % Employee.empCount)  
  
    def display_employee(self):  
        print('Имя: {}. Зарплата: {}'.format(self.name, self.salary))  
  
  
print("Employee.__doc__:", Employee.__doc__)  
print("Employee.__name__:", Employee.__name__)  
print("Employee.__module__:", Employee.__module__)  
print("Employee.__bases__:", Employee.__bases__)  
print("Employee.__dict__:", Employee.__dict__) 

Когда этот код выполняется, он возвращает такой результат:

Employee.__doc__: Базовый класс для всех сотрудников
Employee.__name__: Employee
Employee.__module__: __main__
Employee.__bases__: (,)
Employee.__dict__: {'__module__': '__main__', '__doc__': 'Базовый класс для всех сотрудников', 'emp_count': 0, '__init__': , 'display_count': , 'display_employee': , '__dict__': , '__weakref__': }

Удаление объектов (сбор мусора)

Python автоматически удаляет ненужные объекты (встроенные типы или экземпляры классов), чтобы освободить пространство памяти. С помощью процесса ‘Garbage Collection’ Python периодически восстанавливает блоки памяти, которые больше не используются.

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

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

a = 40  # создали объект <40>
b = a  # увеличивает количество ссылок  <40> 
c = [b]  # увеличивает количество ссылок <40> 

del a  # уменьшает количество ссылок <40>
b = 100  # уменьшает количество ссылок <40> 
c[0] = -1  # уменьшает количество ссылок <40>

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

Пример работы __del__()
Деструктор __del__() выводит имя класса того экземпляра, который должен быть уничтожен:

class Point:  
    def __init__(self, x=0, y=0):  
        self.x = x  
        self.y = y  
  
    def __del__(self):  
        class_name = self.__class__.__name__  
        print('{} уничтожен'.format(class_name))  
  
  
pt1 = Point()  
pt2 = pt1  
pt3 = pt1  
print(id(pt1), id(pt2), id(pt3))  # выведите id объектов  
del pt1  
del pt2  
del pt3

Когда вышеуказанный код выполняется и выводит следующее:

17692784 17692784 17692784
Point уничтожен

В идеале вы должны создавать свои классы в отдельном модуле. Затем импортировать их в основной модуль программы с помощью import SomeClass.

Наследование — это процесс, когда один класс наследует атрибуты и методы другого. Класс, чьи свойства и методы наследуются, называют Родителем или Суперклассом. А класс, свойства которого наследуются — класс-потомок или Подкласс.

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

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

Синтаксис наследования класса

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

class SubClassName(ParentClass1[, ParentClass2, ...]):
	"""Необязательная строка документации класса""" 
	class_suite

Пример наследования класса в Python

class Parent:  # объявляем родительский класс  
    parent_attr = 100  
  
    def __init__(self):  
        print('Вызов родительского конструктора')  
  
    def parent_method(self):  
        print('Вызов родительского метода')  
  
    def set_attr(self, attr):  
        Parent.parent_attr = attr  
  
    def get_attr(self):  
        print('Атрибут родителя: {}'.format(Parent.parent_attr))  
  
  
class Child(Parent):  # объявляем класс наследник  
    def __init__(self):  
        print('Вызов конструктора класса наследника')  
  
    def child_method(self):  
        print('Вызов метода класса наследника')  
  
  
c = Child()  # экземпляр класса Child  
c.child_method()  # вызов метода child_method  
c.parent_method()  # вызов родительского метода parent_method  
c.set_attr(200)  # еще раз вызов родительского метода  
c.get_attr()  # снова вызов родительского метода

Когда этот код выполняется, он выводит следующий результат:

Вызов конструктора класса наследника
Вызов метода класса наследника
Вызов родительского метода
Атрибут родителя: 200

Аналогичным образом вы можете управлять классом с помощью нескольких родительских классов:

class A:        # объявите класс A
...

class B:        # объявите класс B
...

class C(A, B):  # C наследуется от A и B
...

Вы можете использовать функции issubclass() или isinstance() для проверки отношений двух классов и экземпляров.

  • Логическая функция issubclass(sub, sup) возвращает значение True, если данный подкласс sub действительно является подклассом sup.
  • Логическая функция isinstance(obj, Class) возвращает True, если obj является экземпляром класса Class или является экземпляром подкласса класса.

Переопределение методов

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

Пример переопределения методов:

class Parent:  # объявите родительский класс  
    def my_method(self):  
        print('Вызов родительского метода')  
  
  
class Child(Parent):  # объявите класс наследник  
    def my_method(self):  
        print('Вызов метода наследника')  

  
c = Child()  # экземпляр класса Child  
c.my_method()  # метод переопределен классом наследником

Когда этот код выполняется, он производит следующий результат:

Вызов метода наследника

Популярные базовые методы

В данной таблице перечислены некоторые общие функции. Вы можете переопределить их в своих собственных классах.

Метод, описание и пример вызова
1 __init__(self [, args...]) — конструктор (с любыми необязательными аргументами)
obj = className(args)
2 __del__(self) — деструктор, удаляет объект
del obj
3 __repr__(self) — программное представление объекта
repr(obj)
4 __str__(self) — строковое представление объекта
str(obj)

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

Предположим, вы создали класс Vector для представления двумерных векторов. Что происходит, когда вы используете дополнительный оператор для их добавления? Скорее всего, Python будет против.

Однако вы можете определить метод __add__ в своем классе для добавления векторов и оператор + будет вести себя так как нужно.

class Vector:   
    def __init__(self, a, b):   
        self.a = a   
        self.b = b   
    
    def __str__(self):    
        return 'Vector ({}, {})'.format(self.a, self.b)  
    
    def __add__(self, other):   
        return Vector(self.a + other.a, self.b + other.b)   
     
     
v1 = Vector(2, 10)   
v2 = Vector(5, -2)    
print(v1 + v2)

При выполнении этого кода, мы получим:

Vector(7, 8)

Приватные методы и атрибуты

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

Пример приватного атрибута:

class JustCounter:  
    __secret_count = 0  
  
    def count(self):  
        self.__secret_count += 1  
        print(self.__secret_count)  
  
  
counter = JustCounter()  
counter.count()  
counter.count()  
print(counter.__secret_count)

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

1
2
Traceback (most recent call last):
   File "test.py", line 12, in 
      print(counter.__secret_count)
AttributeError: 'JustCounter' object has no attribute '__secret_count'

Вы можете получить доступ к таким атрибутам, так object._className__attrName. Если вы замените свою последнюю строку следующим образом, то она будет работать.

...
print(counter._JustCounter__secret_count)

При выполнении кода, получаем результат:

1
2
2

Тест на понимание классов

Какое их представленных слов лучше всего подходит для названия класса?

Параметры функции range могут быть отрицательными целыми числами

Какое из следующих утверждений описывает эту строку?

xyz = Circle()

переменной присваивается значение класса

создается экземпляр класса

Как написать атрибут класса?

Как вызвать метод swim(5) у экземпляра hero?

Что происходит при наследовании Apple от Fruit?

В Apple доступны все атрибуты класса Fruit

Apple перезаписывает класс Fruit

В Fruit доступны все атрибуты класса Apple

Fruit перезаписывает класс Apple

На чтение 32 мин Просмотров 5.3к. Опубликовано 28.02.2022

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

Содержание

  1. Процедурно-ориентированный стиль
  2. Объектно-ориентированный стиль
  3. Абстракция
  4. Инкапсуляция
  5. Наследование
  6. Полиморфизм
  7. Создание класса в Python
  8. Атрибут:
  9. Статические и динамические атрибуты класса
  10. Метод:
  11. Инициализатор:
  12. Что такое self?
  13. Уровни доступа атрибута и метода
  14. Свойства
  15. Сравнение объектов
  16. Атрибуты функции
  17. Встроенные атрибуты класса
  18. Составляющие класса или объекта
  19. Наследование
  20. super()
  21. Переопределение
  22. Документирование классов
  23. Удаление объектов (сбор мусора)

Процедурно-ориентированный стиль

Процедурно-ориентированная парадигма – это такой подход к программированию, когда код строится на основе функций в математическом смысле этого слова.

Объектно-ориентированный стиль

ООП в Python 3 подразумевает построение кода, оперирующего такими понятиями как python классы и объекты, при этом сама программа создается как некоторая совокупность объектов, которые взаимодействую друг с другом. Объектно-ориентированное программирование ещё долгое время будет являться передовой, если даже не основной, парадигмой программирования. Прямая связь ООП с реальным миром позволяет глубже понимать устройство и принципы работы, как самого языка, так и написания кода в целом, а так же облегчает проектирование внутренней архитектуры.

В данной парадигме есть ряд принципов, которые необходимо знать и понимать.

Абстракция

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

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

Согласно этому принципу вся реализация скрывается внутри абстракции, а внешние пользователи взаимодействуют только с интерфейсом. Это даёт разработчику свободу менять различные детали реализации, не боясь, что у кого-то из пользователей что-то сломается. С другой стороны, всё, что входит в интерфейс (публичные атрибуты) должно быть сохранено в неприкосновенности – каждый пользователь должен быть уверен, что интерфейс не изменится со временем.

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

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

Полиморфизм

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

Создание класса в Python

Классы создаются следующим образом:


class MyClass(BaseClass):

    pass

То есть указывается:

— ключевое слово class

— имя класса

— в круглых скобках имя класса-родителя (если родителя нет, скобки не пишутся)

— непосредственно тело класса

Атрибут:

Атрибут — это переменная, принадлежащая классу. Другое название – поле класса.

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

Атрибуты бывают динамическим и статическим. Статический атрибут относится к самому классу. Динамический принадлежит объектам класса. Пример статического атрибута:


class Car:
    number_of_wheels = 4


print('number_of_wheels:', Car.number_of_wheels)
# Вывод:

number_of_wheels: 4

Здесь мы получаем доступ к атрибуту number_of_wheels, обращаясь к классу напрямую.

Динамический атрибут:


class Car:

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
print(lada.color)
# Вывод:
Red

Здесь мы сперва создаём объект lada класса Car , а затем получаем доступ к атрибуту color, обращаясь к объекту класса.

При этом, можно получить доступ к статическому атрибуту через объект, но к динамическому через класс нельзя:


class Car:
    number_of_wheels = 4

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
print(lada.number_of_wheels)
print(Car.color)
# Вывод:
4

Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 11, in <module>

    print(Car.color)

AttributeError: type object 'Car' has no attribute 'color'

 

Process finished with exit code 1

Существует особенность работы со статическими атрибутами, про которую не стоит забывать: если изменить данное значение у объекта, то оно изменится только для данного объекта. Если изменить значение статического атрибута у класса – оно изменится для всех объектов. Если значение атрибута было изменено, оно становится невосприимчивым к изменениям атрибута у класса. Посмотрите на пример:


class Car:
    number_of_wheels = 4

    def __init__(self, color='Red', speed=250):
        self.color = color
        self.speed = speed


lada = Car()
kia = Car()
lada.number_of_wheels = 3
print('lada:', lada.number_of_wheels)
print('kia:', kia.number_of_wheels)
Car.number_of_wheels = 5
print('Car.number_of_wheels = 5')
print('lada:', lada.number_of_wheels)
print('kia:', kia.number_of_wheels)
# Вывод:
lada: 3

kia: 4

Car.number_of_wheels = 5

lada: 3

kia: 5

Сперва мы создали два объекта класса Car: lada и kia. Затем изменили количество колёс у объекта lada. Если посмотреть на первые две строки вывода, становится ясно, что данный атрибут изменился у объекта lada, но остался прежним у объекта kia. Далее меняем количество колёс для всего класса Car. Обратимся к последним двум строкам вывода. У объекта kia значение атрибута изменилось на то, которое теперь установлено у класса, однако, у объекта lada значение по-прежнему 3.

Ещё одним интересным моментом является то, что атрибуты можно создавать уже после создания объекта, обращаясь к ним «через точку»:


class Car:

    def __init__(self):
        self.direction = 'не определено'

    def drive(self):
        return f'Еду. Направление - {self.direction}'


lada = Car()
lada.drive = 'Восток'
lada.цвет = 'Розовенький'
print('Направление:', lada.drive)
print('Цвет:', lada.цвет)

# Вывод:
Направление: Восток

Цвет: Розовенький

Здесь мы добавляем новый атрибут «цвет» уже после создания объекта lada.

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


def example():
    print('Йа пример')

class Car:

    def __init__(self):
        self.direction = 'не определено'

    def drive(self):
        return f'Еду. Направление - {self.direction}'


lada = Car()
lada.example = example
lada.example()

# Вывод:
Йа пример

В данном случае мы заранее определили функцию example(), потом добавили ссылку на неё в атрибут example уже созданного объекта lada и вызвали его. Но! Всё это чёрная магия и делать так не стоит.

Метод:

Метод – это функция, принадлежащая классу. Вызывается через обращение к объекту класса. Если опуститься к низкоуровневым подробностям, то метод – это атрибут, который можно вызвать (то есть имеет свой атрибут __call__). Первым аргументом всегда передаётся ссылка на объект, которую принято называть в Python self. Рассмотрим пример:


class Car:

    def drive(self, direction):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive('Москва'))
# Вывод:
Еду. Направление – Москва

Здесь мы определили метод drive, имеющий два параметра: ссылку на объект (self ) и направление движения (direction). Запомните, когда мы вызываем метод у объекта, ссылка на объект, для которого вызывается метод, автоматически передаётся в аргументы.

Кроме обычных методов бывают ещё статические и методы класса. Они объявляются в теле класса при помощи декораторов @staticmethod и @classmethod соответственно.

Статические методы аналогичны обычным функциям. Они не получают ссылку ни на объект, ни на класс, а следовательно, не имеют доступ к их состояниям. Единственное отличие от обычных функций – это то, что они относятся к пространству имён класса. Вызвать такой метод можно как через класс, так и через его объекты:


class Car:
    @staticmethod
    def drive(direction):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive('Москва'))
print(Car.drive('Юг'))
# Вывод:
Еду. Направление - Москва

Еду. Направление – Юг

Метод класса получает первым аргументом ссылку на класс, которую принято называть cls. Такие методы относятся к классам, а значит, могут оперировать статическими атрибутами, но не имеют доступа к объектам. Однако, вызывать их можно и через обращение к объектам. Пример:


class Car:
    direction = 'Москва'
    @classmethod
    def drive(cls):
        return f'Еду. Направление - {cls.direction}'

lada = Car()
print(lada.drive())
print(Car.drive())
# Вывод:
Еду. Направление - Москва

Еду. Направление – Москва

Обращение к статическому атрибуту внутри метода класса осуществляется через обращение к ссылке на класс cls. Если попытаться обратиться к динамическому атрибуту, получим исключение:


class Car:

    @classmethod
    def drive(cls):
        return f'Еду. Направление - {self.direction}'

    def __init__(self, direction):
        self.direction = direction

lada = Car('Москва')
print(lada.drive())
print(Car.drive())
# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 11, in <module>

    print(lada.drive())

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 5, in drive

return f'Еду. Направление - {self.direction}'

NameError: name 'self' is not defined

 

Process finished with exit code 1

Инициализатор:

Инициализатор – метод объекта, который вызывается сразу после его создания. В Пайтоне он представлен дандер методом __init__. Этот дандер метод Вы будете использовать и встречать в чужом коде чаще всего в объектно-ориентированном программировании. Как и у всех методов экземпляра класса, первым аргументом передаётся self. Аргументы, которые перечислены в инициализаторе, должны быть переданы при создании объекта в круглых скобках:


class Car:

    def __init__(self, direction):
        print('Направление:', direction)

lada = Car('Москва')
# Вывод:
Направление: Москва

В приведённом выше примере мы не присваиваем полученный аргумент объекту, а значит, не можем получить к нему доступ обратившись к объекту:


class Car:

    def __init__(self, direction):
        pass
lada = Car('Москва')
print(lada.direction)
# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 6, in <module>

    print(lada.direction)

AttributeError: 'Car' object has no attribute 'direction'

 

Process finished with exit code 1

Чтобы это стало возможным необходимо самостоятельно создать атрибут объекта (при помощи self) и присвоить ему значение:


class Car:

    def __init__(self, direction):
        self.direction = direction
lada = Car('Москва')
print(lada.direction)
# Вывод:
Москва

Стоит помнить, что __init__() не должен возвращать никаких значений, иначе:


class Car:

    def __init__(self, direction):
        self.direction = direction
        return 1
    
lada = Car('Москва')
print(lada.direction)
# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 7, in <module>

    lada = Car('Москва')

TypeError: __init__() should return None, not 'int'

 

Process finished with exit code 1

В некоторых языках программирования инициализаторов может быть несколько. В Python у класса __init__() указывается только один раз. Аргументам, указанным в __init__() можно задать значения по умолчанию, как и у любой функции.

Кроме инициализатора __init__() есть ещё и конструктор класса – метод класса __new__(). Он вызывается перед созданием объекта. Но его используют очень редко. Выглядит это так:


class Car:
    def __new__(cls, *args):
        print('Сейчас создам объект')
        return super(Car, cls).__new__(cls)

    def __init__(self, direction):
        self.direction = direction

lada = Car('Москва')
print(lada.direction)
# Вывод:
Сейчас создам объект

Москва

Что такое self?

Вы уже видели этот аргумент в примерах выше. self – это ссылка на текущий экземпляр класса (объект). Если Вы знакомы с другими языками программирования, активно использующими объектно-ориентированный стиль, то можете узнать в self аналог this. При помощи этой ссылки Вы можете обратится к атрибутам и методам объекта внутри класса, то есть в момент, когда объект ещё не существует. Стоит отметить, что self, как и cls (такая же ссылка, но не на объект, а на класс) – не ключевые слова языка Питон, а всеобщая договорённость. Другими словами, вместо self и cls можно использовать любое другое слово, но это не приветствуется – Вы ведь хотите, чтоб Вас понимали другие? Пример:


class Car:

    def __init__(ссылка_на_объект, direction):
        ссылка_на_объект.direction = direction

lada = Car('Москва')
print(lada.direction)
# Вывод:
Москва

В этом примере я заменил self на «ссылка_на_объект» и всё продолжает работать штатно.

Поскольку это просто переменная, хранящая ссылку, её можно возвращать из метода, менять, присваивать атрибутам и тому подобное. На таких «фокусах», к примеру, построен паттерн «builder». Немного трюков:


class A:

    def give_self(self):
        return self

class B(A):
    pass

class C(A):

    def __init__(self):
        self.i = self

    def say(self):
        print('Hello from C!')

a = A()
b = B()
c = C()

print('A:', a)
print('B:', b)
print('C:', c)

b = a.give_self()
print('B:', b)
print('A == B:', a == b)
print('A is B:', a is b)
c.i.i.i.i.i.i.i.i.say()
a = c.give_self()
print('a.i.i.i.i.i.i.i.i.say():')
a.i.i.i.i.i.i.i.i.say()
# Вывод:
A: <__main__.A object at 0x000002681DE91FD0>

B: <__main__.B object at 0x000002681DE91FA0>

C: <__main__.C object at 0x000002681DE91F70>

B: <__main__.A object at 0x000002681DE91FD0>

A == B: True

A is B: True

Hello from C!

a.i.i.i.i.i.i.i.i.say():

Hello from C!

Уровни доступа атрибута и метода

В отличие от многих других строгих языков программирования, в Python инкапсуляция и уровни доступа реализованы на уровне договорённостей. Это означает, что программист лишь устанавливает маркер в имени атрибута в виде одного или двух нижних подчёркиваний в начале имени и он означает, что это приватный атрибут. Использовать его извне не стоит, но такая возможность остаётся. Это часть философии Пайтона – мы сами несём ответственность за свои действия. Подробнее об уровнях доступа можно прочитать в нашем уроке про PEP8.

Свойства

Свойство – метод, который «снаружи» выглядит как атрибут. Вот так:


class Car:

    @property
    def drive(self, direction='Север'):
        return f'Еду. Направление - {direction}'

lada = Car()
print(lada.drive)

# Вывод:
Еду. Направление – Север

В этом листинге мы использовали декоратор @property. Видите, обращение к методу drive теперь выполнено без круглых скобок в конце? Да, метод волшебным образом превратился в атрибут. Однако, изменить его значение не получится:


class Car:

    @property
    def drive(self, direction='Север'):
        return f'Еду. Направление - {direction}'

lada = Car()
lada.drive = 'Восток'
print(lada.drive)

# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 8, in <module>

    lada.drive = 'Восток'

AttributeError: can't set attribute

 

Process finished with exit code 1

Смысл свойств в том, что мы можем создать геттеры и сеттеры для таких методов.

— Геттер – функция, которая вызывается при попытке получить значение свойства

— Сеттер – функция, которая вызывается при попытке изменить значение свойства

Пример:


class Car:

    def set_direction(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            self.direction = 'не определено'
            return
        self.direction = direction

    def get_drive(self):
        return f'Еду. Направление - {self.direction}'

    drive = property(get_drive, set_direction)

lada = Car()
lada.drive = 'Восток'
print(lada.drive)

# Вывод:
Еду. Направление – Восток

Здесь мы определили сеттер set_direction() и геттер get_drive(), а после этого создали свойство drive при помощи функции property. Сеттеры чаще всего используются для валидации вводимых аргументов, как и в этом примере. Теперь, если свойству попытаться установить значение не подходящего типа, сеттер не даст это сделать:


class Car:

    def set_direction(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            self.direction = 'не определено'
            return
        self.direction = direction

    def get_drive(self):
        return f'Еду. Направление - {self.direction}'

    drive = property(get_drive, set_direction)

lada = Car()
lada.drive = 1
print(lada.drive)

# Вывод:
<class 'int'> is not string

Еду. Направление - не определено

Для удобства можно использовать не функцию property(), а декоратор @property:


class Car:

    def __init__(self):
        self.direction = 'не определено'

    @property
    def drive(self):
        return f'Еду. Направление - {self.direction}'

    @drive.setter
    def drive(self, direction):
        if type(direction) != str:
            print(type(direction), 'is not string')
            return

        self.direction = direction

    @drive.deleter
    def drive(self):
        print('Пока!')


lada = Car()
lada.drive = 'Восток'
print(lada.drive)
del lada.drive

# Вывод:
Еду. Направление - Восток

Пока!

Декорируемый @property метод drive становится геттером (именно поэтому в первом примере мы не смогли изменить значение – был определён только геттер). Какой метод считать сеттером обозначаем декоратором @drive.setter. @drive.deleter отмечает метод, который будет вызван при удалении.

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

Применение свойств повышает надёжность Вашего кода и считается хорошим тоном. Есть и другая сторона – они увеличивают размер кода. Использовать ли этот инструмент Вам придётся решать самостоятельно исходя из поставленных задач.

Сравнение объектов

Оператор is нужен, чтобы узнать, ссылаются ли два объекта на одно и то же место в памяти. Он вернет True, если это так. Оператор is not вернет True, если сравнить 2 объекта, которые ссылаются на разные места в памяти.


class Car:

    pass

lada = Car()
kia = Car()
print('lada is kia:', lada is kia)
print('lada is not kia:', lada is not kia)
zhigul = lada
print('lada is zhigul:', lada is zhigul)
print('lada is not zhigul:', lada is not zhigul)

# Вывод:
lada is kia: False

lada is not kia: True

lada is zhigul: True

lada is not zhigul: False

Атрибуты функции

Обычно получать доступ к атрибутам объекта можно с помощью оператора «точка» (например, ‘Строка’.__doc__). Но Python предоставляет возможность делать это и с помощью встроенных функций:

— getattr() — Возвращает значение атрибута или значение по умолчанию, если первое не было указано

— hasattr() – проверяет, есть ли у объекта аргумент, переданный в функцию вторым аргументом

— setattr – устанавливает значение атрибута или, если такого атрибута нет, создаёт его.

— delattr – удаляет указанный атрибут


class Car:
    pass

lada = Car()
setattr(lada, 'цвет', 'Розовенький')
print('hasattr:', hasattr(lada, 'цвет'))
print('getattr:', getattr(lada, 'цвет'))
delattr(lada, 'цвет')
print('hasattr:', hasattr(lada, 'цвет'))

# Вывод:
hasattr: True

getattr: Розовенький

hasattr: False

Встроенные атрибуты класса

Python classes содержат встроенные атрибуты, которые хранят некоторую полезную информацию.

  • __dict__ — словарь, содержащий пространство имен класса.

class Car:
    pass

lada = Car()
print('__dict__:', Car.__dict__)

# Вывод:
__dict__: {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Car' objects>, '__weakref__': <attribute '__weakref__' of 'Car' objects>, '__doc__': None}

  • __doc__ — строка документации класса. None если, документация отсутствует.

class Car:
    '''Это строка документации'''
    pass

lada = Car()
print('__doc__:', Car.__doc__)

# Вывод:
__doc__: Это строка документации

  • __name__ — имя класса.

class Car:
    pass

lada = Car()
print('__name__:', Car.__name__)

# Вывод:
__name__: Car

  • __module__ — имя модуля, в котором определяется класс.

class Car:
    pass

lada = Car()
print('__module__:', Car.__module__)

# Вывод:
__module__: __main__

  • __bases__ — кортеж, содержащий базовые классы, в порядке их появления. Кортеж будет пустым, если наследование не было.

class A:
    pass

class B(A):
    pass

class C(B, A):
    pass

print('__bases__:', C.__bases__)

# Вывод:
__bases__: (<class '__main__.B'>, <class '__main__.A'>)

  • __mro__ — Порядок разрешения методов в множественном наследовании.

class A:
    pass

class B(A):
    pass

class C(B, A):
    pass

print('__mro__:', C.__mro__)

# Вывод:
__mro__: (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

Составляющие класса или объекта

В Python присутствует функция dir, которая выводит список всех методов, атрибутов и переменных класса или объекта.


class C():
    atr_1 = 1

    def __init__(self, atr_2=2):
        self.atr_2 = atr_2

c = C()
setattr(c, 'atr_3', 3)
print('dir(C):', dir(C))
print('dir(c):', dir(c))

# Вывод:
dir(C): ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'atr_1']

dir(c): ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'atr_1', 'atr_2', 'atr_3']

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

Мы уже говорили о наследовании, как о принципе объектно-ориентированного подхода к программированию в начале статьи. Напомню, что наследование – это когда мы создаём новый класс на основе другого. Такой класс-потомок получает (наследует) от класса-родителя его методы и атрибуты. В момент создания некоторые атрибуты и методы изменяют (расширяют). Так же классу-потомку можно добавить новые атрибуты и методы. Давайте посмотрим, как это делается на практике:


class A():

    def __init__(self, atr='Атрибут класса A'):
        self.atr = atr

    def method_A(self):
        return 'Метод класса A'

class B(A):

    def method_B(self):
        return 'Метод класса B'


b = B()
print('b.atr:', b.atr)
print('b.method_A():', b.method_A())
print('b.method_B():', b.method_B())

# Вывод:
b.atr: Атрибут класса A

b.method_A(): Метод класса A

b.method_B(): Метод класса B

В этом примере мы создали класс А. Этот класс будет родителем. Затем создаём класс В, унаследованный от класса А. Для этого при объявлении класса В указываем «А» в скобках. Теперь объекты класса В имеют, как атрибуты и методы родительского класса (atr, method_A), так и свои собственные (method_B), что явно следует из вывода программы.

У класса может быть несколько классов-родителей. Такое наследование называется множественным:


class A():

    def method_A(self):
        return 'Метод класса A'

class B():

    def method_B(self):
        return 'Метод класса B'


class C(A, B):

    def method_C(self):
        return 'Метод класса C'

c = C()
print('c.method_A():', c.method_A())
print('c.method_B():', c.method_B())
print('c.method_C():', c.method_C())

# Вывод:
c.method_A(): Метод класса A

c.method_B(): Метод класса B

c.method_C(): Метод класса C

Как Вы можете видеть, при множественном наследовании класс-потомок (класс С) получает атрибуты и методы всех своих родителей (method_A() от класса А и method_В() от класса В).

Проверить что один класс является потомком другого можно при помощи функции issubclass(). Все классы в Python являются наследниками от класса object. Давайте в этом убедимся:


class A():
    pass

print('isinstance(A, object):', isinstance(A, object))

# Вывод:
isinstance(A, object): True

Первый аргумент – имя класса, который проверяем, второй – предполагаемый класс-родитель.

super()

super – это функция, которая возвращает ссылку на родительский класс (точнее, имитирует её при помощи прокси-объекта). Через эту ссылку можно обращаться к методам класса-родителя:


class A():
    def method_A(self):
        print('method А')

class B(A):
    def method_B(self):
        print('method В')
        super().method_A()

b = B()
b.method_B()

# Вывод:
method В

method А

Здесь мы вызываем родительский метод method_A() внутри собственного метода method_B() класса В. Это удобно тем, что мы не заботимся об имени родителя. super() означает «родитель, как бы он не назывался».

Переопределение

Переопределение методов и атрибутов (его ещё называют перегрузкой) – практическая реализация принципа полиморфизма. Чтобы это сделать, достаточно объявить в классе-наследнике метод с тем же названием, что и в базовом классе:


class A():
    def method_A(self):
        print('method А')

class B(A):
    def method_A(self):
        print('method В')

b = B()
b.method_A()

# Вывод:
method В

В этом примере мы в классе В() переопределили унаследованный от класса А метод method_A(). Таким образом, класс наследник может расширять функционал класса родителя.

Для чего это нужно?

— Причина №1. Чтоб не дублировать код. Представьте, что у Вас есть несколько классов, имеющих некоторые одинаковые атрибуты. Следуя принципу «не повторяйся» стоит создать класс-родитель, в нём определить эти общие атрибуты и сделать все схожие классы его потомками:


# Было:
class Овчарка():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

class Питбуль():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

овчарка = Овчарка()
питбуль = Питбуль()

овчарка.фас()
питбуль.фас()
овчарка.служить()

# Вывод:
<class '__main__.Овчарка'>: Р-р-ррр!

<class '__main__.Питбуль'>: Р-р-ррр!

<class '__main__.Овчарка'>: Гав!


# Выделяем повторяющийся метод в класс-родитель:

class Собака():
    def фас(self):
        print(self.__class__, end=': ')
        print('Р-р-ррр!')

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

class Питбуль(Собака):
    pass

овчарка = Овчарка()
питбуль = Питбуль()

овчарка.фас()
питбуль.фас()
овчарка.служить()

# Вывод:
<class '__main__.Овчарка'>: Р-р-ррр!

<class '__main__.Питбуль'>: Р-р-ррр!

<class '__main__.Овчарка'>: Гав!

— Причина №2. Для декларирования интерфейса. Часто создают класс, в котором перечислены методы (пустые) и от него наследуются другие классы, в которых переопределяются методы. В месте, где над объектами классов-потомков производятся какие-то манипуляции, сперва проверяют что эти классы действительно являются потомками базового класса, определённого в начале. Такие базовые классы называют абстрактными. Всё это необходимо для того, чтоб быть уверенным, что в объектах реализованы необходимые методы. Простой пример:


class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

овчарка = Овчарка()
if issubclass(овчарка.__class__, Собака):
    print(овчарка.цвет())
овчарка.служить()

# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 14, in <module>

    print(овчарка.цвет())

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 4, in цвет

    assert False

AssertionError

Здесь мы не только создали базовый класс, но и добавили вызов исключения в метод «цвет». Это заставляет все классы-потомки переопределить данный метод:


class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')

    @property
    def цвет(self):
        self._цвет = 'Странный'
        return self._цвет

овчарка = Овчарка()
if issubclass(овчарка.__class__, Собака):
    print('цвет:', овчарка.цвет)
овчарка.служить()

# Вывод:
цвет: Странный

<class '__main__.Овчарка'>: Гав!

Обратите внимание на условие if issubclass(). Оно позволяет удостоверится что объект принадлежит к классу, унаследованному от класса Собака, а значит содержит метод «цвет». Конечно, всегда остаются возможности выстрелить себе в ногу:


class Собака():

    def цвет(self):
        assert False

class Овчарка(Собака):

    def служить(self):
        print(self.__class__, end=': ')
        print('Гав!')


овчарка = Овчарка()

del овчарка.цвет

if issubclass(овчарка.__class__, Собака):
    print('цвет:', овчарка.цвет)
овчарка.служить()

# Вывод:
Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 14, in <module>

    del овчарка.цвет

AttributeError: цвет

 

Process finished with exit code 1

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

Документирование классов

В Python есть замечательный инструмент под названием строки документации или docstrings. Это инструмент, позволяющий создавать многострочное описание функций и классов, которое автоматически сохраняется как атрибут объекта. Для их создания необходимо указать описание в тройных одинарных кавычках в самом начале тела класса или функции. Доступ к данной документации можно получить через дандер атрибут __doc__ или функцию help():


class Собака():
    '''Друг человека'''
    pass


мопс = Собака()
print('__doc__:', мопс.__doc__)
print('n', 'help:')
help(мопс)
# Вывод:
__doc__: Друг человека

 

help:

Help on Собака in module __main__ object:

 

class Собака(builtins.object)

|  Друг человека

|

|  Data descriptors defined here:

|

|  __dict__

|      dictionary for instance variables (if defined)

|

|  __weakref__

|      list of weak references to the object (if defined)

Подробнее об этом Вы можете узнать в нашем уроке Комментарии в Python.

Удаление объектов (сбор мусора)

В Python можно удалить ссылку на любой объект. Для этого используется ключевое слово del. При этом у объекта счётчик ссылок уменьшится на 1. Когда счётчик ссылок достигнет нуля, внутренний механизм языка под названием сборщик мусора (Garbage Collector) удалит объект из памяти.

За то, как будет произведено удаление ссылки, отвечает дандер метод __del__() (называется деструктор) и его можно переопределить:


class Собака():

    def __del__(self):
        print(f'Удаляю {self.__class__.__name__}')


мопс = Собака()
второй_мопс = мопс
del мопс
print(второй_мопс)
print(мопс)
# Вывод:
Удаляю Собака

<__main__.Собака object at 0x000002496713A9D0>

 

Traceback (most recent call last):

File "C:UsersDushenkoAppDataRoamingJetBrainsPyCharm2021.3scratchesscratch.py", line 11, in <module>

print(мопс)

NameError: name 'мопс' is not defined

 

Process finished with exit code 1

Здесь мы добавили в деструктор вывод в консоль об удалении объекта. Обратите внимание на вывод. После удаления переменная «мопс» с ссылкой на объект класса «Собака» не существует. Об этом говорит исключение «NameError». Однако, ссылка на тот же объект в переменной «второй_мопс» всё ещё жива, так что объект всё ещё в памяти.

Прежде чем приступить к теории, давайте решим следующую задачу.

Напишем программу, которая будет моделировать объекты класса «автомобиль». При моделировании необходимо определить степень детализации объектов, которая зависит от действий, выполняемых этими объектами.

  • Пусть все автомобили имеют разный цвет.
  • Двигатель можно запустить, если в баке есть топливо.
  • Двигатель можно заглушить.
  • На автомобиле можно отправиться в путь на N километров при соблюдении следующих условий: двигатель запущен и запас топлива в баке и средний расход позволяют проехать этот путь.
  • После поездки запас топлива уменьшается в соответствии со средним расходом.
  • Автомобиль можно заправить до полного бака в любой момент времени.

Выделим важные для нашей программы свойства объектов класса: цвет, средний расход топлива, объём топливного бака, запас топлива, общий пробег.

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

Попробуем описать объекты этого класса с помощью коллекций и функций:

def create_car(color, consumption, tank_volume, mileage=0):
    return {
        "color": color,
        "consumption": consumption,
        "tank_volume": tank_volume,
        "reserve": tank_volume,
        "mileage": mileage,
        "engine_on": False
    }


def start_engine(car):
    if not car["engine_on"] and car["reserve"] > 0:
        car["engine_on"] = True
        return "Двигатель запущен."
    return "Двигатель уже был запущен."


def stop_engine(car):
    if car["engine_on"]:
        car["engine_on"] = False
        return "Двигатель остановлен."
    return "Двигатель уже был остановлен."


def drive(car, distance):
    if not car["engine_on"]:
        return "Двигатель не запущен."
    if car["reserve"] / car["consumption"] * 100 < distance:
        return "Малый запас топлива."
    car["mileage"] += distance
    car["reserve"] -= distance / 100 * car["consumption"]
    return f"Проехали {distance} км. Остаток топлива: {car['reserve']} л."


def refuel(car):
    car["reserve"] = car["tank_volume"]


def get_mileage(car):
    return f"Пробег {car['mileage']} км."


def get_reserve(car):
    return f"Запас топлива {car['reserve']} л."


car_1 = create_car(color="black", consumption=10, tank_volume=55)

print(start_engine(car_1))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 300))
print(get_mileage(car_1))
print(get_reserve(car_1))
print(stop_engine(car_1))
print(drive(car_1, 100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

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

Объектно-ориентированное программирование (ООП) позволяет устранить недостатки процедурного подхода. Язык программирования Python является объектно-ориентированным. Это означает, что каждая сущность (переменная, функция и так далее) в этом языке является объектом определенного класса. Ранее мы говорили, что, например, целое число является в Python типом данных int. На самом деле есть класс целых чисел int.

Убедимся в этом, написав простую программу:

print(type(1))

Вывод программы:

<class 'int'>

Синтаксис создания класса в Python выглядит следующим образом:

class <ИмяКласса>:
    <описание класса>

Имя класса по стандарту PEP8 записывается в стиле CapWords (каждое слово с прописной буквы).

Давайте перепишем пример про автомобили с использованием ООП. Создадим класс Car и пока оставим в нём инструкцию-заглушку pass:

class Car:
    pass

В классах описываются свойства объектов и действия объектов или совершаемые действия над ними.

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

<имя_объекта>.<имя_атрибута> = <значение>

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

def <имя_метода>(self, <аргументы>):
    <тело метода>

В методах первым аргументом всегда идёт объект self. Он является объектом, для которого вызван метод. self позволяет использовать внутри описания класса атрибуты объекта в методах и вызывать сами методы.

Во всех классах Python есть специальный метод __init()__, который вызывается при создании объекта. В этом методе происходит инициализация всех атрибутов класса. В методы можно передавать аргументы. Вернёмся к нашему примеру и создадим в классе метод __init()__, который будет при создании автомобиля принимать его свойства как аргументы:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

Итак, мы создали класс автомобилей и описали метод __init__() для инициализации его объектов. Для создания объекта класса нужно использовать следующий синтаксис:

<имя_объекта> = <ИмяКласса>(<аргументы метода __init__>)

Создадим в программе автомобиль класса Car. Для этого добавим следующую строку в основной код программы после описания класса, отделив от класса согласно PEP8 двумя пустыми строками:

car_1 = Car(color="black", consumption=10, tank_volume=55)

Обратите внимание: наш код стало легче читать, потому что мы видим, что создаётся объект определённого класса, а не просто вызывается функция, из которой возвращается значение-словарь.

Опишем с помощью методов какие действия могут совершать объекты класса Car. По PEP8 между объявлением методов нужно поставить одну пустую строку.

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve


car_1 = Car(color="black", consumption=10, tank_volume=55)
print(car_1.start_engine())
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(300))
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")
print(car_1.stop_engine())
print(car_1.drive(100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

Обратите внимание: взаимодействие с объектом класса вне описания класса осуществляется только с помощью методов и прямого доступа к атрибутам не происходит. Этот принцип ООП называется инкапсуляцией.

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

car_1 = Car(color="black", consumption=10, tank_volume=55)
car_1.mileage = 1000
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")

Вывод программы:

Пробег 1000 км.
Запас топлива 55 л.

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

class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

Напишем функцию range_reserve(), которая будет определять для автомобилей классов Car и ElectricCar запас хода в километрах. Функции, которые могут работать с объектами разных классов, называются полиморфными. А сам принцип ООП называется полиморфизмом.

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

Запас хода в километрах можно вычислить, разделив запас топлива (или заряд батареи) на расход и умножив результат на 100. Определить запас топлива или заряд батареи можно с помощью метода get_reserve(). Для соблюдения принципа инкапсуляции добавим метод get_consumption() в оба класса для получения значения атрибута consumption. Тогда полиморфная функция запишется так:

def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100

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

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100


car_1 = Car(color="black", consumption=10, tank_volume=55)
car_2 = ElectricCar(color="white", consumption=15, bat_capacity=90)
print(f"Запас хода: {range_reserve(car_1)} км.")
print(f"Запас хода: {range_reserve(car_2)} км.")

Вывод программы:

Запас хода: 550.0 км.
Запас хода: 600.0 км.

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

Понравилась статья? Поделить с друзьями:
  • Как написать свой кейлоггер на python
  • Как написать свой квест
  • Как написать свой калькулятор на python
  • Как написать свой калькулятор на java
  • Как написать свой итератор python