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

В этой статье вы сделаете первый шаг к объектно-ориентированному программированию — подходу, который лежит в основе современных языков разработки, включая Python. Вы узнаете, что такое классы и объекты, как создавать собственные типы данных и описывать поведение объектов с помощью методов. Мы разберёмся с ключевыми понятиями ООП — атрибутами, инкапсуляцией и полиморфизмом — и увидим, как они помогают писать более структурированный, гибкий и масштабируемый код. На простом примере — модели автомобиля — вы научитесь создавать классы, задавать свойства объектов, реализовывать действия и проектировать интерфейс взаимодействия с объектами. Эта статья станет основой для понимания более сложных концепций ООП, с которыми вы познакомитесь в следующих частях главы.

Ключевые вопросы статьи

  • Что такое класс и объект в Python и как они связаны между собой?
  • Как задать свойства и поведение объектов с помощью атрибутов и методов?
  • Как работает метод __init__()?
  • Что такое инкапсуляция и как она помогает защитить внутреннее состояние объекта?
  • Как реализовать полиморфизм в Python и в чём суть подхода «утиная типизация»?

Что такое класс и объект в Python и как они связаны между собой

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

Пусть каждый автомобиль имеет собственный цвет. Его двигатель можно запустить, если в баке есть топливо, и заглушить в любой момент. Автомобиль должен проезжать определённое расстояние — но только при условии, что двигатель включён, а топлива хватает на заданный путь. После каждой поездки запас топлива уменьшается. Также автомобиль можно заправить до полного бака.

Выделим ключевые характеристики объектов: цвет, средний расход топлива, объём бака, запас топлива, пробег. А также — действия: запуск и остановка двигателя, поездка, заправка.

На текущем этапе мы можем описать такой объект с помощью словаря и управлять им через отдельные функции. Ниже — пример реализации:

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 построен на объектной модели. Это значит, что практически всё в нём — объекты, созданные на основе классов. Например, число 1 — не просто значение, а объект встроенного класса int. Убедимся в этом:

print(type(1))

# Вывод программы:
# <class 'int'>

Всё в Python — объекты: числа, строки, функции, списки и даже модули. А самое важное — вы можете создавать свои собственные классы для описания любых сущностей, от автомобилей до пользователей и заказов.

Дальше мы увидим, как именно создаются классы и как они позволяют сделать код более чистым, гибким и надёжным.

Как задать свойства и поведение объектов с помощью атрибутов и методов

Создание собственных классов — основа объектно-ориентированного программирования. С их помощью можно задать как структуру объекта (его свойства), так и поведение (набор доступных действий).

Синтаксис определения класса в Python выглядит так:

class <ИмяКласса>:
    <описание класса>

Согласно стандарту оформления PEP 8, имя класса записывается в стиле CapWords — каждое слово начинается с заглавной буквы и не содержит подчёркиваний.

Например: Car, ElectricVehicle, UserProfile.

Давайте перепишем наш пример с автомобилем, используя классы. Начнём с самого простого — создадим пустой класс Car, в котором пока ничего не будет происходить. В этом случае используется инструкция-заглушка pass:

class Car:
    pass

Теперь разберёмся, как задать свойства и поведение объектов, создаваемых на основе этого класса.

Атрибуты — свойства объекта

Атрибутами называются переменные, которые хранятся внутри объекта. Они описывают состояние конкретного экземпляра класса — например, цвет, пробег, запас топлива и т. д.

Атрибут можно создать вручную после создания объекта:

<имя_объекта>.<имя_атрибута> = <значение>

Пример:

car.color = "чёрный"

Методы: как описать действия объекта

Методы — это функции, определённые внутри класса. Они описывают поведение объекта, например запуск двигателя, поездку или заправку.
Метод объявляется с помощью ключевого слова def, а первым его параметром всегда идёт self. Это ссылка на конкретный объект, для которого вызывается метод:

def start_engine(self):
    ...

С помощью self метод может обращаться к атрибутам и другим методам того же объекта. Без self объект не сможет «узнать» собственные свойства.

Как работает метод init ()

Во всех классах Python можно определить специальный метод __init__(). Он автоматически вызывается при создании нового объекта и отвечает за начальную инициализацию его атрибутов.
Вернёмся к примеру с автомобилем и добавим метод __init__() в класс Car, чтобы задать значения свойств при создании:

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

Здесь мы определили, что каждый объект класса Car будет обладать следующими атрибутами: цветом (color), средним расходом топлива (consumption), объёмом бака (tank_volume), текущим запасом топлива (reserve), пробегом (mileage) и состоянием двигателя (engine_on). Значения этих атрибутов задаются при создании объекта.

Для создания объекта на основе класса используют следующую запись:

<имя_объекта> = <ИмяКласса>(<аргументы метода __init__()>)

Пример:

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

Примечание

Теперь всё, что относится к автомобилю, собрано внутри одного объекта. Такой подход делает код не только короче, но и понятнее, масштабируемее и легче в поддержке.

Что такое инкапсуляция и как она помогает защитить внутреннее состояние объекта

Теперь, когда мы описали свойства и поведение класса, наш код стало легче читать. Мы видим, что создаётся объект определённого типа (Car), а не просто передаётся словарь в набор функций. Вся логика работы с автомобилем теперь сосредоточена внутри самого класса.
Опишем с помощью методов, какие действия может выполнять объект 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 л.
Двигатель остановлен.
Двигатель не запущен.

Примечание

Все действия с объектом происходят через методы. Мы не изменяем значения атрибутов напрямую, а работаем с объектом через предусмотренный «интерфейс» — это и есть принцип инкапсуляции.

Инкапсуляция означает, что внутреннее состояние объекта скрыто от внешнего кода и доступно только через методы. Такой подход помогает:

  • избежать случайных ошибок при прямом изменении данных;
  • сохранить корректность логики внутри класса;
  • упростить отладку и поддержку программы.

Посмотрим, что произойдёт, если проигнорировать этот принцип и напрямую изменить атрибут mileage, не выполняя никаких проверок:

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 л.

Как видите, объект «проехал» 1000 км, хотя двигатель не запускался и топливо не расходовалось. Это делает данные недостоверными и нарушает логику работы модели. Именно от таких ситуаций инкапсуляция и защищает.

Позже мы узнаем, как ещё сильнее ограничивать доступ к данным с помощью специальных соглашений и модификаторов доступа.

Как реализовать полиморфизм в Python и в чём суть подхода «утиная типизация»

В предыдущем примере мы описали класс Car. Теперь давайте создадим ещё один класс — для электромобилей. Он будет отличаться от бензинового аналога тем, что вместо топливного бака у него есть аккумуляторная батарея:

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. Такое поведение называется полиморфизмом — когда один и тот же интерфейс (в нашем случае — функция) работает с объектами разных типов.

Чтобы это было возможно, в обоих классах должны быть методы с одинаковыми именами и сигнатурами. Для расчёта запаса хода нам понадобится ещё один метод — get_consumption():

def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100

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

Это поведение получило своё название от шутливого выражения:

Примечание

«Если что-то выглядит как утка, плавает как утка и крякает как утка — значит, это утка»

Применительно к Python это значит: если объект умеет делать всё, что от него ожидается, мы можем с ним работать — даже не проверяя его тип с помощью isinstance.

Вот полная программа с двумя классами и функцией range_reserve():

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 км.

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

Что дальше

Изучив статью, вы научились создавать собственные классы и описывать поведение объектов с помощью методов. Вы поняли, как задаются свойства через атрибуты, как работает метод __init__().

Вы узнали, что такое инкапсуляция и как она помогает защитить внутреннее состояние объектов от неконтролируемых изменений. А ещё — познакомились с полиморфизмом и подходом «утиная типизация», который делает Python особенно гибким.

Далее мы расширим эти знания: научимся использовать наследование, переопределять методы и работать со специальными («волшебными») методами классов. Всё это позволит строить более мощные и выразительные структуры в вашем коде.

Ключевые выводы статьи

  • Класс — это шаблон, по которому создаются объекты с заданными свойствами и поведением.
  • Объект — экземпляр класса, который хранит собственные данные и умеет выполнять методы.
  • Атрибуты описывают состояние объекта, а методы — его действия.
  • Метод __init__() и параметр self используются для инициализации и работы с объектом.
  • Инкапсуляция и полиморфизм помогают создавать надёжные и гибкие программы.