В прошлом параграфе мы узнали, что такое классы, и научились их создавать. Мы также столкнулись с тем, что при создании похожих классов появляется дублирование кода. В ООП для создания новых классов на основе других применяется принцип наследования.
Наследование позволяет при создании нового класса указать для него базовый класс. От базового класса наследуется вся его структура — атрибуты и методы. Созданный класс-наследник называется производным классом.
Покажем принцип наследования на примере. Напишем класс «Карандаш» Pencil
, который в качестве атрибута хранит цвет карандаша. Карандашом можно нарисовать рисунок. Также напишем класс «Ручка» Pen
, который тоже хранит цвет, но кроме создания рисунка может ещё и подписать документ, если цвет ручки синий, чёрный или фиолетовый.
class Pencil:
def __init__(self, color="серый"):
self.color = color
def draw_picture(self):
return f"Нарисован рисунок цветом '{self.color}'."
class Pen(Pencil):
def sign_document(self):
if self.color not in ("синий", "чёрный", "фиолетовый"):
return f"Ручкой цвета '{self.color}' нельзя подписать документ."
return f"Подписан документ."
blue_pen = Pen(color="синий")
print(blue_pen.draw_picture())
print(blue_pen.sign_document())
red_pen = Pen(color="красный")
print(red_pen.draw_picture())
print(red_pen.sign_document())
Вывод программы:
Нарисован рисунок цветом 'синий'. Подписан документ. Нарисован рисунок цветом 'красный'. Ручкой цвета 'красный' нельзя подписать документ.
Класс Pen
является производным от базового класса Pencil
. За счёт этого мы не описывали заново методы __init__
и draw_picture
и они работают так же, как и в базовом классе. Атрибут color
тоже унаследован из базового класса Pencil
. Интерпретатор при вызове метода или атрибута сначала ищет их в текущем производном классе. Если их нет в текущем классе, происходит поиск в базовом классе. Если и в базовом их нет, происходит поиск в вышестоящем базовом классе (в базовом классе для текущего базового класса). И так далее, пока метод или атрибут не будет найден в одном из базовых классов. Иначе программа выдаст ошибку класса AttributeError
.
Добавим в классе «Ручка» возможность указать тип ручки: шариковая, гелевая, перьевая и т. д. И пусть подписать документ можно любой ручкой, кроме гелевой. Для получения типа ручки нам нужно модифицировать метод __init__
, добавив в него аргумент pen_type
и сохранив его значение в атрибуте. Таким образом, нам нужно дополнить метод базового класса. Такая операция при наследовании называется расширением метода.
При расширении методов необходимо вначале вызвать метод базового класса с помощью функции super()
. Если этого не сделать, то не будут созданы атрибуты базового класса в производном классе, и это приведёт к ошибке отсутствия атрибутов.
Модифицируем нашу программу:
class Pencil:
def __init__(self, color="серый"):
self.color = color
def draw_picture(self):
return f"Нарисован рисунок цветом '{self.color}'."
class Pen(Pencil):
def __init__(self, color, pen_type):
super().__init__(color=color)
self.pen_type = pen_type
def sign_document(self):
if self.color not in ("синий", "чёрный", "фиолетовый"):
return f"Ручкой цвета '{self.color}' нельзя подписать документ."
elif self.pen_type == "гелевая":
return f"Ручкой типа '{self.pen_type}' нельзя подписать документ."
return f"Подписан документ."
blue_ball_pen = Pen(color="синий", pen_type="шариковая")
print(blue_ball_pen.draw_picture())
print(blue_ball_pen.sign_document())
blue_gel_pen = Pen(color="синий", pen_type="гелевая")
print(blue_gel_pen.draw_picture())
print(blue_gel_pen.sign_document())
Вывод программы:
Нарисован рисунок цветом 'синий'. Подписан документ. Нарисован рисунок цветом 'синий'. Ручкой типа 'гелевая' нельзя подписать документ.
Если в производном классе метод базового класса переписывается заново, то говорят о переопределении метода. Переопределим метод draw_picture
так, чтобы он выводил информацию о типе ручки, которой нарисован рисунок. В класс Pen
нужно добавить следующий код:
def draw_picture(self):
return f"Нарисован рисунок ручкой типа '{self.pen_type}', цветом '{self.color}'."
Вывод программы с переопределённым методом:
Нарисован рисунок ручкой типа 'шариковая', цветом 'синий'. Подписан документ. Нарисован рисунок ручкой типа 'гелевая', цветом 'синий'. Ручкой типа 'гелевая' нельзя подписать документ.
Наследование может производиться сразу от нескольких классов. В таком случае базовые классы перечисляются через запятую. Производный класс унаследует атрибуты и методы обоих базовых классов.
Напишем программу, в которой будут следующие классы:
GreetingFormal
. При инициализации объектов этого класса создаётся атрибутformal_greeting
, содержащий строку «Добрый день,». В этом классе также есть методgreet_formal
, который принимает аргументname
и возвращает строку с приветствием по имени.GreetingInformal
. При инициализации объектов этого класса создаётся атрибутinformal_greeting
, содержащий строку «Привет,». В этом классе также есть методgreet_informal
, который принимает аргументname
и возвращает строку с приветствием по имени.GreetingMix
. Этот класс наследуется от двух предыдущих и может приветствовать пользователя по имени обоими методами.
Программа запишется так:
class GreetingFormal:
def __init__(self):
self.formal_greeting = "Добрый день,"
def greet_formal(self, name):
return f"{self.formal_greeting} {name}!"
class GreetingInformal:
def __init__(self):
self.informal_greeting = "Привет,"
def greet_informal(self, name):
return f"{self.informal_greeting} {name}!"
class GreetingMix(GreetingFormal, GreetingInformal):
def __init__(self):
GreetingFormal.__init__(self)
GreetingInformal.__init__(self)
mixed_greeting = GreetingMix()
print(mixed_greeting.greet_formal("Пользователь"))
print(mixed_greeting.greet_informal("Пользователь"))
Вывод программы:
Добрый день, Пользователь! Привет, Пользователь!
Обратите внимание на метод __init__
класса GreetingMix
. В нём вместо вызова метода базового класса через функцию super()
используется непосредственный вызов из базовых классов с указанием имён этих классов. Такой вызов необходим из-за того, что метод __init__
присутствует в обоих базовых классах и происходит конфликт. Интерпретатор при использовании функции super()
в нашем примере использовал бы метод того класса, который стоит левее при перечислении в объявлении производного класса. В нашем примере это привело бы к тому, что __init__
из класса GreetingInformal
не был бы вызван и в производном классе не произошла бы инициализация атрибута informal_greeting
. Тогда при вызове метода greet_informal
было бы вызвано исключение AttributeError
.
На основе операции наследования перепишем пример про автомобили из прошлого параграфа. Пусть класс ElectricCar
наследуется от класса Car
. Методы __init__
и drive
будут переопределены, метод recharge
создан в производном классе, а остальные методы и атрибуты наследуются без изменений.
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(Car):
def __init__(self, color, consumption, bat_capacity, mileage=0):
super().__init__(color, consumption, bat_capacity, mileage)
self.bat_capacity = bat_capacity
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
electric_car = ElectricCar(color="white", consumption=15, bat_capacity=90)
print(electric_car.start_engine())
print(electric_car.drive(100))
Описание класса ElectricCar
существенно сократилось за счёт использования наследования.
Давайте посмотрим, что выведет функция print
, если передать в неё объект созданного нами класса ElectricCar
. Добавим в программу следующий код:
print(electric_car)
Вывод программы:
<__main__.ElectricCar object at 0x000002365DDD8A00>
Такой вывод говорит нам только о том, что переменная electric_car
является объектом класса ElectricCar
и расположена по определённому адресу в памяти. Можно этот вывод сделать более информативным. Когда в функцию print
для вывода передаётся аргумент, не являющийся строкой, к нему применяется стандартная функция str
. При этом в классе, к которому относится аргумент, для аргумента вызывается специальный (ещё говорят «магический») метод __str__
. Остаётся только описать, какую строку вернёт этот метод. И тогда это значение и будет выводиться функцией print
. Дополним класс ElectricCar
методом __str__
:
def __str__(self):
return f"Электромобиль. " \
f"Цвет: {self.color}. " \
f"Пробег: {self.mileage} км. " \
f"Остаток заряда: {self.reserve} кВт*ч."
Проверим, как будет работать наш код:
electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
print(electric_car.start_engine())
print(electric_car.drive(100))
print(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
в начале имени от методов без этих букв:
class A:
def __init__(self):
self.value = 10
def __add__(self, other):
return "Выполняется метод __add__."
def __radd__(self, other):
return "Выполняется метод __radd__."
def __iadd__(self, other):
self.value += other
return self
def __str__(self):
return f"value: {self.value}."
a = A()
print(a + 1)
print(1 + a)
a += 1
print(a)
Вывод программы:
Выполняется метод __add__. Выполняется метод __radd__. value: 11.
Для операции a + 1
был использован метод __add__
. Для операции 1 + a
был использован метод __radd__
. А для операции +=
использован __iadd__
. Обратите внимание: при выполнении методов, начинающихся с буквы i
, недостаточно только изменить атрибуты объекта, нужно ещё вернуть объект из метода, иначе в объект запишется None
.
Напишем метод __repr__
для класса ElectricCar
:
def __repr__(self):
return f"ElectricCar('{self.color}', " \
f"{self.consumption}, " \
f"{self.bat_capacity}, " \
f"{self.mileage})"
Код для проверки работы метода:
electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
print(repr(electric_car))
electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
print([electric_car, electric_car_1])
Вывод программы:
ElectricCar('белый', 15, 90, 0) [ElectricCar('белый', 15, 90, 0), ElectricCar('чёрный', 17, 80, 0)]
Опишем операцию сложения для объектов класса ElectricCar
: возвращается новый объект класса ElectricCar
, у которого цвет такой же, как у левого слагаемого, а уровень заряда батареи, ёмкость батареи, расход энергии на 100 километров пути и общий пробег вычисляются как сумма соответствующих атрибутов слагаемых объектов:
def __add__(self, other):
new_car = ElectricCar(self.color,
self.consumption + other.consumption,
self.bat_capacity + other.bat_capacity,
self.mileage + other.mileage)
new_car.reserve = self.reserve + other.reserve
return new_car
Код для проверки метода:
electric_car = ElectricCar(color="белый", consumption=15, bat_capacity=90)
electric_car_1 = ElectricCar(color="чёрный", consumption=17, bat_capacity=80)
electric_car.start_engine()
electric_car_1.start_engine()
electric_car.drive(300)
electric_car_1.drive(100)
new_electric_car = electric_car + electric_car_1
print(new_electric_car)
Вывод программы:
Электромобиль. Цвет: белый. Пробег: 400 км. Остаток заряда: 108.0 кВт*ч.
Ещё по теме
Полный список специальных методов с описанием можно посмотреть в документации.