Прежде чем приступить к теории, давайте решим следующую задачу.
Напишем программу, которая будет моделировать объекты класса «Автомобиль». При моделировании необходимо определить степень детализации объектов, которая зависит от действий, выполняемых этими объектами.
- Пусть все автомобили имеют разный цвет.
- Двигатель можно запустить, если в баке есть топливо.
- Двигатель можно заглушить.
- На автомобиле можно отправиться в путь на N километров при соблюдении следующих условий: двигатель запущен и запас топлива в баке и средний расход позволяют проехать этот путь.
- После поездки запас топлива уменьшается в соответствии со средним расходом.
- Автомобиль можно заправить до полного бака в любой момент времени.
Выделим важные для нашей программы свойства объектов класса: цвет, средний расход топлива, объём топливного бака, запас топлива, общий пробег.
Определим, какие действия может выполнять объект: запустить двигатель, проехать N километров, остановить двигатель, заправить автомобиль. Пока наши знания позволяют использовать в качестве объекта в программе словарь.
Попробуем описать объекты этого класса с помощью коллекций и функций:
1def create_car(color, consumption, tank_volume, mileage=0):
2 return {
3 "color": color,
4 "consumption": consumption,
5 "tank_volume": tank_volume,
6 "reserve": tank_volume,
7 "mileage": mileage,
8 "engine_on": False
9 }
10
11
12def start_engine(car):
13 if not car["engine_on"] and car["reserve"] > 0:
14 car["engine_on"] = True
15 return "Двигатель запущен."
16 return "Двигатель уже был запущен."
17
18
19def stop_engine(car):
20 if car["engine_on"]:
21 car["engine_on"] = False
22 return "Двигатель остановлен."
23 return "Двигатель уже был остановлен."
24
25
26def drive(car, distance):
27 if not car["engine_on"]:
28 return "Двигатель не запущен."
29 if car["reserve"] / car["consumption"] * 100 < distance:
30 return "Малый запас топлива."
31 car["mileage"] += distance
32 car["reserve"] -= distance / 100 * car["consumption"]
33 return f"Проехали {distance} км. Остаток топлива: {car['reserve']} л."
34
35
36def refuel(car):
37 car["reserve"] = car["tank_volume"]
38
39
40def get_mileage(car):
41 return f"Пробег {car['mileage']} км."
42
43
44def get_reserve(car):
45 return f"Запас топлива {car['reserve']} л."
46
47
48car_1 = create_car(color="black", consumption=10, tank_volume=55)
49
50print(start_engine(car_1))
51print(drive(car_1, 100))
52print(drive(car_1, 100))
53print(drive(car_1, 100))
54print(drive(car_1, 300))
55print(get_mileage(car_1))
56print(get_reserve(car_1))
57print(stop_engine(car_1))
58print(drive(car_1, 100))
Вывод программы:
Двигатель запущен. Проехали 100 км. Остаток топлива: 45.0 л. Проехали 100 км. Остаток топлива: 35.0 л. Проехали 100 км. Остаток топлива: 25.0 л. Малый запас топлива. Пробег 300 км. Запас топлива 25.0 л. Двигатель остановлен. Двигатель не запущен.
Мы описали все действия над объектом с помощью функций. Такой подход в программировании называется процедурным и долгое время был популярным. Он позволяет эффективно решать простые задачи. Однако при усложнении задачи и появлении новых объектов процедурный подход приводит к дублированию и ухудшению читаемости кода.
Объектно-ориентированное программирование (ООП) позволяет устранить недостатки процедурного подхода. Язык программирования Python является объектно-ориентированным. Это означает, что каждая сущность (переменная, функция и т. д.) в этом языке является объектом определённого класса. Ранее мы говорили, что, например, целое число является в Python типом данных int
. На самом деле есть класс целых чисел int
.
Убедимся в этом, написав простую программу:
1print(type(1))
Вывод программы:
<class 'int'>
Синтаксис создания класса в Python выглядит следующим образом:
1class <ИмяКласса>:
2 <описание класса>
Имя класса по стандарту PEP 8 записывается в стиле CapWords (каждое слово с прописной буквы).
Давайте перепишем пример про автомобили с использованием ООП. Создадим класс Car
и пока оставим в нём инструкцию-заглушку pass
:
1class Car:
2 pass
В классах описываются свойства объектов и действия объектов или совершаемые над ними действия.
Свойства объектов называются атрибутами. По сути атрибуты — переменные, в значениях которых хранятся свойства объекта. Для создания или изменения значения атрибута необходимо использовать следующий синтаксис:
1<имя_объекта>.<имя_атрибута> = <значение>
Действия объектов называются методами. Методы очень похожи на функции, в них можно передавать аргументы и возвращать значения с помощью оператора return
, но вызываются методы после указания конкретного объекта. Для создания метода используется следующий синтаксис:
1def <имя_метода>(self, <аргументы>):
2 <тело метода>
В методах первым аргументом всегда идёт объект self
. Он является объектом, для которого вызван метод. self
позволяет использовать внутри описания класса атрибуты объекта в методах и вызывать сами методы.
Во всех классах Python есть специальный метод __init__()
, который вызывается при создании объекта. В этом методе происходит инициализация всех атрибутов класса. В методы можно передавать аргументы. Вернёмся к нашему примеру и создадим в классе метод __init__()
, который будет при создании автомобиля принимать его свойства как аргументы:
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
Итак, мы создали класс автомобилей и описали метод __init__()
для инициализации его объектов. Для создания объекта класса нужно использовать следующий синтаксис:
1<имя_объекта> = <ИмяКласса>(<аргументы метода __init__()>)
Создадим в программе автомобиль класса Car
. Для этого добавим следующую строку в основной код программы после описания класса, отделив от класса, согласно PEP 8, двумя пустыми строками:
1car_1 = Car(color="black", consumption=10, tank_volume=55)
Обратите внимание: наш код стало легче читать, потому что мы видим, что создаётся объект определённого класса, а не просто вызывается функция, из которой возвращается значение-словарь.
Опишем с помощью методов, какие действия могут совершать объекты класса Car
. По PEP 8, между объявлением методов нужно поставить одну пустую строку.
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
42car_1 = Car(color="black", consumption=10, tank_volume=55)
43print(car_1.start_engine())
44print(car_1.drive(100))
45print(car_1.drive(100))
46print(car_1.drive(100))
47print(car_1.drive(300))
48print(f"Пробег {car_1.get_mileage()} км.")
49print(f"Запас топлива {car_1.get_reserve()} л.")
50print(car_1.stop_engine())
51print(car_1.drive(100))
Вывод программы:
Двигатель запущен. Проехали 100 км. Остаток топлива: 45.0 л. Проехали 100 км. Остаток топлива: 35.0 л. Проехали 100 км. Остаток топлива: 25.0 л. Малый запас топлива. Пробег 300 км. Запас топлива 25.0 л. Двигатель остановлен. Двигатель не запущен.
Обратите внимание: взаимодействие с объектом класса вне описания класса осуществляется только с помощью методов, прямого доступа к атрибутам не происходит. Этот принцип ООП называется инкапсуляцией.
Инкапсуляция заключается в сокрытии внутреннего устройства класса за интерфейсом, состоящим из методов класса. Это необходимо, чтобы не нарушать логику работы методов внутри класса. Если не следовать принципу инкапсуляции и попытаться взаимодействовать с атрибутами напрямую, то могут происходить изменения, которые приведут к ошибкам. Например, если в нашем примере попытаться изменить пробег напрямую, а не с помощью метода drive()
, то автомобиль проедет указанный путь даже с пустым баком и без расхода топлива:
1car_1 = Car(color="black", consumption=10, tank_volume=55)
2car_1.mileage = 1000
3print(f"Пробег {car_1.get_mileage()} км.")
4print(f"Запас топлива {car_1.get_reserve()} л.")
Вывод программы:
Пробег 1000 км. Запас топлива 55 л.
Давайте напишем ещё один класс для электромобилей. Их отличие будет заключаться в замене топливного бака на заряд аккумуляторной батареи:
1class ElectricCar:
2
3 def __init__(self, color, consumption, bat_capacity, mileage=0):
4 self.color = color
5 self.consumption = consumption
6 self.bat_capacity = bat_capacity
7 self.reserve = bat_capacity
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 recharge(self):
33 self.reserve = self.bat_capacity
34
35 def get_mileage(self):
36 return self.mileage
37
38 def get_reserve(self):
39 return self.reserve
Напишем функцию range_reserve()
, которая будет определять для автомобилей классов Car
и ElectricCar
запас хода в километрах. Функции, которые могут работать с объектами разных классов, называются полиморфными. А сам принцип ООП называется полиморфизмом.
Говоря о полиморфизме в Python, стоит упомянуть принятую в этом языке так называемую «утиную типизацию» (Duck typing). Она получила своё название от шутливого выражения: «Если нечто выглядит как утка, плавает как утка и крякает как утка, это, вероятно, утка и есть» («If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck»). В программах на Python это означает, что, если какой-то объект поддерживает все требуемые от него операции, с ним и будут работать с помощью этих операций, не заботясь о том, какого он на самом деле типа.
Чтобы функция работала для объектов обоих классов, необходимо предусмотреть одинаковый интерфейс в классах. Это означает, что методы классов, используемые в функции, должны называться одинаково, принимать одинаковые аргументы и возвращать значения одного типа данных.
Запас хода в километрах можно вычислить, разделив запас топлива (или заряд батареи) на расход и умножив результат на 100. Определить запас топлива или заряд батареи можно с помощью метода get_reserve()
. Для соблюдения принципа инкапсуляции добавим метод get_consumption()
в оба класса для получения значения атрибута consumption
. Тогда полиморфная функция запишется так:
1def range_reserve(car):
2 return car.get_reserve() / car.get_consumption() * 100
Полностью программа с классами, полиморфной функцией и пример их использования представлены ниже:
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:
46
47 def __init__(self, color, consumption, bat_capacity, mileage=0):
48 self.color = color
49 self.consumption = consumption
50 self.bat_capacity = bat_capacity
51 self.reserve = bat_capacity
52 self.mileage = mileage
53 self.engine_on = False
54
55 def start_engine(self):
56 if not self.engine_on and self.reserve > 0:
57 self.engine_on = True
58 return "Двигатель запущен."
59 return "Двигатель уже был запущен."
60
61 def stop_engine(self):
62 if self.engine_on:
63 self.engine_on = False
64 return "Двигатель остановлен."
65 return "Двигатель уже был остановлен."
66
67 def drive(self, distance):
68 if not self.engine_on:
69 return "Двигатель не запущен."
70 if self.reserve / self.consumption * 100 < distance:
71 return "Малый заряд батареи."
72 self.mileage += distance
73 self.reserve -= distance / 100 * self.consumption
74 return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."
75
76 def recharge(self):
77 self.reserve = self.bat_capacity
78
79 def get_mileage(self):
80 return self.mileage
81
82 def get_reserve(self):
83 return self.reserve
84
85 def get_consumption(self):
86 return self.consumption
87
88
89def range_reserve(car):
90 return car.get_reserve() / car.get_consumption() * 100
91
92
93car_1 = Car(color="black", consumption=10, tank_volume=55)
94car_2 = ElectricCar(color="white", consumption=15, bat_capacity=90)
95print(f"Запас хода: {range_reserve(car_1)} км.")
96print(f"Запас хода: {range_reserve(car_2)} км.")
Вывод программы:
Запас хода: 550.0 км. Запас хода: 600.0 км.
В нашей программе можно заметить, что классы Car
и ElectricCar
имеют много общих атрибутов и методов. Это привело к дублированию кода. В следующем параграфе мы познакомимся с наследованием — принципом ООП, позволяющим устранить подобную избыточность кода.