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

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

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

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

  • Пусть все автомобили имеют разный цвет.
  • Двигатель можно запустить, если в баке есть топливо.
  • Двигатель можно заглушить.
  • На автомобиле можно отправиться в путь на 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 <ИмяКласса>:
    <описание класса>

Имя класса по стандарту PEP 8 записывается в стиле 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. Для этого добавим следующую строку в основной код программы после описания класса, отделив от класса, согласно PEP 8, двумя пустыми строками:

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

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

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

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 запас хода в километрах. Функции, которые могут работать с объектами разных классов, называются полиморфными. А сам принцип ООП называется полиморфизмом.

Говоря о полиморфизме в 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. Тогда полиморфная функция запишется так:

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

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

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

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

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

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

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