5.1. Объектная модель Python. Классы, поля и методы

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

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

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

  • Пусть все автомобили имеют разный цвет.
  • Двигатель можно запустить, если в баке есть топливо.
  • Двигатель можно заглушить.
  • На автомобиле можно отправиться в путь на 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 имеют много общих атрибутов и методов. Это привело к дублированию кода. В следующем параграфе мы познакомимся с наследованием — принципом ООП, позволяющим устранить подобную избыточность кода.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф4.3. Рекурсия. Декораторы. Генераторы

Здесь мы разберём вопрос создания рекурсивных функций для реализации декларативного подхода при разработке программ. А заодно изучим синтаксис и примеры использования декораторов и генераторов.

Следующий параграф5.2. Волшебные методы, переопределение методов. Наследование

Здесь рассмотрим наследование классов, в том числе множественное наследование, а также «волшебные» методы Python для реализации стандартных операций с объектами создаваемых классов.