В прошлом параграфе мы узнали, что такое классы, и научились их создавать. Мы также столкнулись с тем, что при создании похожих классов появляется дублирование кода. В ООП для создания новых классов на основе других применяется принцип наследования.
Наследование позволяет при создании нового класса указать для него базовый класс. От базового класса наследуется вся его структура — атрибуты и методы. Созданный класс-наследник называется производным классом.
Покажем принцип наследования на примере. Напишем класс «Карандаш» Pencil
, который в качестве атрибута хранит цвет карандаша. Карандашом можно нарисовать рисунок. Также напишем класс «Ручка» Pen
, который тоже хранит цвет, но кроме создания рисунка может ещё и подписать документ, если цвет ручки синий, чёрный или фиолетовый.
1class Pencil:
2
3 def __init__(self, color="серый"):
4 self.color = color
5
6 def draw_picture(self):
7 return f"Нарисован рисунок цветом '{self.color}'."
8
9
10class Pen(Pencil):
11
12 def sign_document(self):
13 if self.color not in ("синий", "чёрный", "фиолетовый"):
14 return f"Ручкой цвета '{self.color}' нельзя подписать документ."
15 return f"Подписан документ."
16
17
18blue_pen = Pen(color="синий")
19print(blue_pen.draw_picture())
20print(blue_pen.sign_document())
21red_pen = Pen(color="красный")
22print(red_pen.draw_picture())
23print(red_pen.sign_document())
Вывод программы:
Нарисован рисунок цветом 'синий'. Подписан документ. Нарисован рисунок цветом 'красный'. Ручкой цвета 'красный' нельзя подписать документ.
Класс Pen
является производным от базового класса Pencil
. За счёт этого мы не описывали заново методы __init__
и draw_picture
и они работают так же, как и в базовом классе. Атрибут color
тоже унаследован из базового класса Pencil
. Интерпретатор при вызове метода или атрибута сначала ищет их в текущем производном классе. Если их нет в текущем классе, происходит поиск в базовом классе. Если и в базовом их нет, происходит поиск в вышестоящем базовом классе (в базовом классе для текущего базового класса). И так далее, пока метод или атрибут не будет найден в одном из базовых классов. Иначе программа выдаст ошибку класса AttributeError
.
Добавим в классе «Ручка» возможность указать тип ручки: шариковая, гелевая, перьевая и т. д. И пусть подписать документ можно любой ручкой, кроме гелевой. Для получения типа ручки нам нужно модифицировать метод __init__
, добавив в него аргумент pen_type
и сохранив его значение в атрибуте. Таким образом, нам нужно дополнить метод базового класса. Такая операция при наследовании называется расширением метода.
При расширении методов необходимо вначале вызвать метод базового класса с помощью функции super()
. Если этого не сделать, то не будут созданы атрибуты базового класса в производном классе, и это приведёт к ошибке отсутствия атрибутов.
Модифицируем нашу программу:
1class Pencil:
2
3 def __init__(self, color="серый"):
4 self.color = color
5
6 def draw_picture(self):
7 return f"Нарисован рисунок цветом '{self.color}'."
8
9
10class Pen(Pencil):
11
12 def __init__(self, color, pen_type):
13 super().__init__(color=color)
14 self.pen_type = pen_type
15
16 def sign_document(self):
17 if self.color not in ("синий", "чёрный", "фиолетовый"):
18 return f"Ручкой цвета '{self.color}' нельзя подписать документ."
19 elif self.pen_type == "гелевая":
20 return f"Ручкой типа '{self.pen_type}' нельзя подписать документ."
21 return f"Подписан документ."
22
23
24blue_ball_pen = Pen(color="синий", pen_type="шариковая")
25print(blue_ball_pen.draw_picture())
26print(blue_ball_pen.sign_document())
27blue_gel_pen = Pen(color="синий", pen_type="гелевая")
28print(blue_gel_pen.draw_picture())
29print(blue_gel_pen.sign_document())
Вывод программы:
Нарисован рисунок цветом 'синий'. Подписан документ. Нарисован рисунок цветом 'синий'. Ручкой типа 'гелевая' нельзя подписать документ.
Если в производном классе метод базового класса переписывается заново, то говорят о переопределении метода. Переопределим метод draw_picture
так, чтобы он выводил информацию о типе ручки, которой нарисован рисунок. В класс Pen
нужно добавить следующий код:
1def draw_picture(self):
2 return f"Нарисован рисунок ручкой типа '{self.pen_type}', цветом '{self.color}'."
Вывод программы с переопределённым методом:
Нарисован рисунок ручкой типа 'шариковая', цветом 'синий'. Подписан документ. Нарисован рисунок ручкой типа 'гелевая', цветом 'синий'. Ручкой типа 'гелевая' нельзя подписать документ.
Наследование может производиться сразу от нескольких классов. В таком случае базовые классы перечисляются через запятую. Производный класс унаследует атрибуты и методы обоих базовых классов.
Напишем программу, в которой будут следующие классы:
GreetingFormal
. При инициализации объектов этого класса создаётся атрибутformal_greeting
, содержащий строку «Добрый день,». В этом классе также есть методgreet_formal
, который принимает аргументname
и возвращает строку с приветствием по имени.GreetingInformal
. При инициализации объектов этого класса создаётся атрибутinformal_greeting
, содержащий строку «Привет,». В этом классе также есть методgreet_informal
, который принимает аргументname
и возвращает строку с приветствием по имени.GreetingMix
. Этот класс наследуется от двух предыдущих и может приветствовать пользователя по имени обоими методами.
Программа запишется так:
1class GreetingFormal:
2
3 def __init__(self):
4 self.formal_greeting = "Добрый день,"
5
6 def greet_formal(self, name):
7 return f"{self.formal_greeting} {name}!"
8
9
10class GreetingInformal:
11
12 def __init__(self):
13 self.informal_greeting = "Привет,"
14
15 def greet_informal(self, name):
16 return f"{self.informal_greeting} {name}!"
17
18
19class GreetingMix(GreetingFormal, GreetingInformal):
20
21 def __init__(self):
22 GreetingFormal.__init__(self)
23 GreetingInformal.__init__(self)
24
25
26mixed_greeting = GreetingMix()
27print(mixed_greeting.greet_formal("Пользователь"))
28print(mixed_greeting.greet_informal("Пользователь"))
Вывод программы:
Добрый день, Пользователь! Привет, Пользователь!
Обратите внимание на метод __init__
класса GreetingMix
. В нём вместо вызова метода базового класса через функцию super()
используется непосредственный вызов из базовых классов с указанием имён этих классов. Такой вызов необходим из-за того, что метод __init__
присутствует в обоих базовых классах и происходит конфликт. Интерпретатор при использовании функции super()
в нашем примере использовал бы метод того класса, который стоит левее при перечислении в объявлении производного класса. В нашем примере это привело бы к тому, что __init__
из класса GreetingInformal
не был бы вызван и в производном классе не произошла бы инициализация атрибута informal_greeting
. Тогда при вызове метода greet_informal
было бы вызвано исключение AttributeError
.
На основе операции наследования перепишем пример про автомобили из прошлого параграфа. Пусть класс ElectricCar
наследуется от класса Car
. Методы __init__
и drive
будут переопределены, метод recharge
создан в производном классе, а остальные методы и атрибуты наследуются без изменений.
1class Car:
2
3 def __init__(self, color, consumption, tank_volume, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.tank_volume = tank_volume
7 self.reserve = tank_volume
8 self.mileage = mileage
9 self.engine_on = False
10
11 def start_engine(self):
12 if not self.engine_on and self.reserve > 0:
13 self.engine_on = True
14 return "Двигатель запущен."
15 return "Двигатель уже был запущен."
16
17 def stop_engine(self):
18 if self.engine_on:
19 self.engine_on = False
20 return "Двигатель остановлен."
21 return "Двигатель уже был остановлен."
22
23 def drive(self, distance):
24 if not self.engine_on:
25 return "Двигатель не запущен."
26 if self.reserve / self.consumption * 100 < distance:
27 return "Малый запас топлива."
28 self.mileage += distance
29 self.reserve -= distance / 100 * self.consumption
30 return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."
31
32 def refuel(self):
33 self.reserve = self.tank_volume
34
35 def get_mileage(self):
36 return self.mileage
37
38 def get_reserve(self):
39 return self.reserve
40
41 def get_consumption(self):
42 return self.consumption
43
44
45class ElectricCar(Car):
46
47 def __init__(self, color, consumption, bat_capacity, mileage=0):
48 super().__init__(color, consumption, bat_capacity, mileage)
49 self.bat_capacity = bat_capacity
50
51 def drive(self, distance):
52 if not self.engine_on:
53 return "Двигатель не запущен."
54 if self.reserve / self.consumption * 100 < distance:
55 return "Малый запас заряда."
56 self.mileage += distance
57 self.reserve -= distance / 100 * self.consumption
58 return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."
59
60 def recharge(self):
61 self.reserve = self.bat_capacity
62
63
64electric_car = ElectricCar(color="white", consumption=15, bat_capacity=90)
65print(electric_car.start_engine())
66print(electric_car.drive(100))
Описание класса ElectricCar
существенно сократилось за счёт использования наследования.
Давайте посмотрим, что выведет функция print
, если передать в неё объект созданного нами класса ElectricCar
. Добавим в программу следующий код:
1print(electric_car)
Вывод программы:
<__main__.ElectricCar object at 0x000002365DDD8A00>
Такой вывод говорит нам только о том, что переменная electric_car
является объектом класса ElectricCar
и расположена по определённому адресу в памяти. Можно этот вывод сделать более информативным. Когда в функцию print
для вывода передаётся аргумент, не являющийся строкой, к нему применяется стандартная функция str
. При этом в классе, к которому относится аргумент, для аргумента вызывается специальный (ещё говорят «магический») метод __str__
. Остаётся только описать, какую строку вернёт этот метод. И тогда это значение и будет выводиться функцией print
. Дополним класс ElectricCar
методом __str__
:
1def __str__(self):
2 return f"Электромобиль. " \
3 f"Цвет: {self.color}. " \
4 f"Пробег: {self.mileage} км. " \
5 f"Остаток заряда: {self.reserve} кВт*ч."
Проверим, как будет работать наш код:
1electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
2print(electric_car.start_engine())
3print(electric_car.drive(100))
4print(electric_car)
Вывод программы:
Двигатель запущен. Проехали 100 км. Остаток заряда: 75.0 кВт*ч. Электромобиль. Цвет: белый. Пробег: 100 км. Остаток заряда: 75.0 кВт*ч.
Специальных методов в Python довольно много. Они нужны для описания взаимодействия с объектами при помощи стандартных операций и встроенных функций. Описание специальных методов называется перегрузкой операторов (operator overloading).
Имена специальных методов выделены слева и справа двумя символами подчёркивания. Как можно заметить, метод __init__
также является специальным.
Рассмотрим назначение некоторых специальных методов.
- Метод
__repr__
вызывается стандартной функциейrepr
и возвращает строку, которая является представлением объекта в формате инициализации. Этот метод может быть также полезен, если необходимо вывести информацию об объектах, когда они являются элементами коллекции. - Методы для операций сравнения:
__lt__(self, other)
—<
;__le__(self, other)
—<=
;__eq__(self, other)
—==
;__ne__(self, other)
—!=
;__gt__(self, other)
—>
;__ge__(self, other)
—>=
.
- Метод
__call__(arg1, arg2, ...)
вызывается, когда сам объект вызывается как функция с аргументами. - Методы для работы с объектом как с коллекцией:
__getitem__(self, key)
используется для получения элемента коллекции по ключуself[key]
;__setitem__(self, key, value)
используется для записи значения по ключуself[key] = value
;__delitem__(self, key)
используется для удаления ключа и соответствующего ему значения;__len__(self)
вызывается стандартной функциейlen
;__contains__(self, item)
вызывается при проверке принадлежности значенияitem
объекту-коллекцииself
с помощью оператораin
.
- Математические операции:
__add__(self, other)
—self + other
;__sub__(self, other)
—self - other
;__mul__(self, other)
—self * other
;__matmul__(self, other)
—self @ other
;__truediv__(self, other)
—self / other
;__floordiv__(self, other)
—self // other
;__mod__(self, other)
—self % other
;__divmod__(self, other)
—divmod(self, other)
;__pow__(self, other)
—self ** other
;__lshift__(self, other)
—self << other
;__rshift__(self, other)
—self >> other
;__and__(self, other)
—self & other
;__xor__(self, other)
—self ^ other
;__or__(self, other)
—self | other
;__radd__(self, other)
—other + self
;__rsub__(self, other)
—other - self
;__rmul__(self, other)
—other * self
;__rmatmul__(self, other)
—other @ self
;__rtruediv__(self, other)
—other / self
;__rfloordiv__(self, other)
—other // self
;__rmod__(self, other)
—other % self
;__rdivmod__(self, other)
—divmod(other, self)
;__rpow__(self, other)
—other ** self
;__rlshift__(self, other)
—other << self
;__rrshift__(self, other)
—other >> self
;__rand__(self, other)
—other & self
;__rxor__(self, other)
—other ^ self
;__ror__(self, other)
—other | self
;__iadd__(self, other)
—self += other
;__isub__(self, other)
—self -= other
;__imul__(self, other)
—self *= other
;__imatmul__(self, other)
—self @= other
;__itruediv__(self, other)
—self /= other
;__ifloordiv__(self, other)
—self //= other
;__imod__(self, other)
—self %= other
;__ipow__(self, other)
—self **= other
;__ilshift__(self, other)
—self <<= other
;__irshift__(self, other)
—self >>= other
;__iand__(self, other)
—self &= other
;__ixor__(self, other)
—self ^= other
;__ior__(self, other)
—self |= other
.
Покажем отличие методов математических операций с буквами r
и i
в начале имени от методов без этих букв:
1class A:
2
3 def __init__(self):
4 self.value = 10
5
6 def __add__(self, other):
7 return "Выполняется метод __add__."
8
9 def __radd__(self, other):
10 return "Выполняется метод __radd__."
11
12 def __iadd__(self, other):
13 self.value += other
14 return self
15
16 def __str__(self):
17 return f"value: {self.value}."
18
19
20a = A()
21print(a + 1)
22print(1 + a)
23a += 1
24print(a)
Вывод программы:
Выполняется метод __add__. Выполняется метод __radd__. value: 11.
Для операции a + 1
был использован метод __add__
. Для операции 1 + a
был использован метод __radd__
. А для операции +=
использован __iadd__
. Обратите внимание: при выполнении методов, начинающихся с буквы i
, недостаточно только изменить атрибуты объекта, нужно ещё вернуть объект из метода, иначе в объект запишется None
.
Напишем метод __repr__
для класса ElectricCar
:
1def __repr__(self):
2 return f"ElectricCar('{self.color}', " \
3 f"{self.consumption}, " \
4 f"{self.bat_capacity}, " \
5 f"{self.mileage})"
Код для проверки работы метода:
1electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
2print(repr(electric_car))
3electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
4print([electric_car, electric_car_1])
Вывод программы:
ElectricCar('белый', 15, 90, 0) [ElectricCar('белый', 15, 90, 0), ElectricCar('чёрный', 17, 80, 0)]
Опишем операцию сложения для объектов класса ElectricCar
: возвращается новый объект класса ElectricCar
, у которого цвет такой же, как у левого слагаемого, а уровень заряда батареи, ёмкость батареи, расход энергии на 100 километров пути и общий пробег вычисляются как сумма соответствующих атрибутов слагаемых объектов:
1def __add__(self, other):
2 new_car = ElectricCar(self.color,
3 self.consumption + other.consumption,
4 self.bat_capacity + other.bat_capacity,
5 self.mileage + other.mileage)
6 new_car.reserve = self.reserve + other.reserve
7 return new_car
Код для проверки метода:
1electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
2electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
3electric_car.start_engine()
4electric_car_1.start_engine()
5electric_car.drive(300)
6electric_car_1.drive(100)
7new_electric_car = electric_car + electric_car_1
8print(new_electric_car)
Вывод программы:
Электромобиль. Цвет: белый. Пробег: 400 км. Остаток заряда: 108.0 кВт*ч.
Ещё по теме
Полный список специальных методов с описанием можно посмотреть в документации.